KondoKit 2.1 + ChatBoxHelmets

This commit is contained in:
downthecrop 2026-01-13 13:28:32 +00:00 committed by Ceikry
parent ae7723b0d2
commit c193f26985
49 changed files with 4890 additions and 1777 deletions

View file

@ -0,0 +1,19 @@
package ChatboxHelmets
// Lifted from KondoHiscores view
data class HiscoresResponse(
val info: PlayerInfo,
val skills: List<Skill>
)
data class PlayerInfo(
val exp_multiplier: String,
val iron_mode: String
)
data class Skill(
val id: String,
val dynamic: String,
val experience: String,
val static: String
)

View file

@ -0,0 +1,198 @@
package ChatboxHelmets
// See JagString.java parse() in the client, There is special encoding
// and this correctly resolves specials.
val SPECIAL_ESCAPES: Map<Char, String> = hashMapOf(
' ' to "(P",
'!' to "(Q",
'"' to "(R",
'#' to "(S",
'$' to "(T",
'%' to "(U",
'&' to "(V",
'\'' to "(W",
'(' to "(X",
')' to "(Y",
'*' to "(Z",
'+' to ")0",
',' to ")1",
'-' to ")2",
'.' to ")3",
'/' to ")4",
':' to "(j",
';' to "(k",
'=' to ")B",
'?' to ")D",
'@' to ")E",
'[' to "*5",
'\\' to "*6",
']' to "*7",
'^' to "*8",
'_' to "*9",
'`' to ")e",
'{' to "*U",
'|' to "*V",
'}' to "*W",
'~' to "*X",
'\u0000' to "(0",
'\u0001' to "(1",
'\u0002' to "(2",
'\u0003' to "(3",
'\u0004' to "(4",
'\u0005' to "(5",
'\u0006' to "(6",
'\u0007' to "(7",
'\b' to "(8",
'\t' to "(9",
'\n' to "(:",
'\u000B' to "(;",
'\u000C' to "(<",
'\r' to "(=",
'\u000E' to "(>",
'\u000F' to "(?",
'\u0010' to "(@",
'\u0011' to "(A",
'\u0012' to "(B",
'\u0013' to "(C",
'\u0014' to "(D",
'\u0015' to "(E",
'\u0016' to "(F",
'\u0017' to "(G",
'\u0018' to "(H",
'\u0019' to "(I",
'\u001A' to "(J",
'\u001B' to "(K",
'\u001C' to "(L",
'\u001D' to "(M",
'\u001E' to "(N",
'\u001F' to "(O",
'\u007F' to "*Y",
'\u0080' to "*Z",
'\u0081' to "+0",
'\u0082' to "+1",
'\u0083' to "+2",
'\u0084' to "+3",
'\u0085' to "+4",
'\u0086' to "+5",
'\u0087' to "+6",
'\u0088' to "+7",
'\u0089' to "+8",
'\u008A' to "+9",
'\u008B' to "*e",
'\u008C' to "*f",
'\u008D' to "*g",
'\u008E' to "*h",
'\u008F' to "*i",
'\u0090' to "*j",
'\u0091' to "*k",
'\u0092' to "+A",
'\u0093' to "+B",
'\u0094' to "+C",
'\u0095' to "+D",
'\u0096' to "+E",
'\u0097' to "+F",
'\u0098' to "+G",
'\u0099' to "+H",
'\u009A' to "+I",
'\u009B' to "+J",
'\u009C' to "+K",
'\u009D' to "+L",
'\u009E' to "+M",
'\u009F' to "+N",
'\u00A0' to "+O",
'\u00A1' to "+P",
'\u00A2' to "+Q",
'\u00A3' to "+R",
'\u00A4' to "+S",
'\u00A5' to "+T",
'\u00A6' to "+U",
'\u00A7' to "+V",
'\u00A8' to "+W",
'\u00A9' to "+X",
'\u00AA' to "+Y",
'\u00AB' to "+Z",
'\u00AC' to ",0",
'\u00AD' to ",1",
'\u00AE' to ",2",
'\u00AF' to ",3",
'\u00B0' to ",4",
'\u00B1' to ",5",
'\u00B2' to ",6",
'\u00B3' to ",7",
'\u00B4' to ",8",
'\u00B5' to ",9",
'\u00B6' to "+e",
'\u00B7' to "+f",
'\u00B8' to "+g",
'\u00B9' to "+h",
'\u00BA' to "+i",
'\u00BB' to "+j",
'\u00BC' to "+k",
'\u00BD' to ",A",
'\u00BE' to ",B",
'\u00BF' to ",C",
'\u00C0' to ",D",
'\u00C1' to ",E",
'\u00C2' to ",F",
'\u00C3' to ",G",
'\u00C4' to ",H",
'\u00C5' to ",I",
'\u00C6' to ",J",
'\u00C7' to ",K",
'\u00C8' to ",L",
'\u00C9' to ",M",
'\u00CA' to ",N",
'\u00CB' to ",O",
'\u00CC' to ",P",
'\u00CD' to ",Q",
'\u00CE' to ",R",
'\u00CF' to ",S",
'\u00D0' to ",T",
'\u00D1' to ",U",
'\u00D2' to ",V",
'\u00D3' to ",W",
'\u00D4' to ",X",
'\u00D5' to ",Y",
'\u00D6' to ",Z",
'\u00D7' to "-0",
'\u00D8' to "-1",
'\u00D9' to "-2",
'\u00DA' to "-3",
'\u00DB' to "-4",
'\u00DC' to "-5",
'\u00DD' to "-6",
'\u00DE' to "-7",
'\u00DF' to "-8",
'\u00E0' to "-9",
'\u00E1' to ",e",
'\u00E2' to ",f",
'\u00E3' to ",g",
'\u00E4' to ",h",
'\u00E5' to ",i",
'\u00E6' to ",j",
'\u00E7' to ",k",
'\u00E8' to "-A",
'\u00E9' to "-B",
'\u00EA' to "-C",
'\u00EB' to "-D",
'\u00EC' to "-E",
'\u00ED' to "-F",
'\u00EE' to "-G",
'\u00EF' to "-H",
'\u00F0' to "-I",
'\u00F1' to "-J",
'\u00F2' to "-K",
'\u00F3' to "-L",
'\u00F4' to "-M",
'\u00F5' to "-N",
'\u00F6' to "-O",
'\u00F7' to "-P",
'\u00F8' to "-Q",
'\u00F9' to "-R",
'\u00FA' to "-S",
'\u00FB' to "-T",
'\u00FC' to "-U",
'\u00FD' to "-V",
'\u00FE' to "-W",
'\u00FF' to "-X"
)

View file

@ -0,0 +1,192 @@
package ChatboxHelmets
import KondoKit.Exposed
import plugin.Plugin
import plugin.api.API
import rt4.Component
import rt4.JagString
import rt4.Player
import java.nio.charset.StandardCharsets
import com.google.gson.Gson
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
class plugin : Plugin() {
private val TARGET_COMPONENT_ID = 8978483
private val TARGET_COMPONENT_INDEX = 51
private val SETTINGS_KEY = "chat-helms"
private val BUBBLE_ICON = "<img=3>"
private val gson = Gson()
val TYPE_ICONS: Map<Int, String> = hashMapOf(
0 to "",
1 to "<img=4>", // IM
2 to "<img=5>", // HCIM
3 to "<img=6>", // UIM
)
@Exposed(description = "Username to account type mappings (0=Normal, 1=IM, 2=HCIM, 3=UIM)")
private var usernameMatches = HashMap<String, Int>()
private var ACCOUNT_TYPE = 0
private var component: Component? = null
override fun Init() {
// Load username matches from local storage
val storedData = API.GetData(SETTINGS_KEY)
usernameMatches = when (storedData) {
is String -> {
try {
gson.fromJson(storedData, HashMap::class.java) as HashMap<String, Int>
} catch (e: Exception) {
println("Failed to deserialize username matches: ${e.message}")
HashMap()
}
}
else -> HashMap()
}
getConfig()
}
override fun OnPluginsReloaded(): Boolean {
cleanup()
return false
}
override fun OnLogin() {
Init()
}
override fun Draw(timeDelta: Long) {
replace()
}
override fun ComponentDraw(componentIndex: Int, component: Component?, screenX: Int, screenY: Int) {
if (component?.id == TARGET_COMPONENT_ID && componentIndex == TARGET_COMPONENT_INDEX) {
this.component = component
}
}
// Allow setting from the chatbox
override fun ProcessCommand(commandStr: String?, args: Array<out String>?) {
super.ProcessCommand(commandStr, args)
when(commandStr) {
"::chathelm" -> {
if (args != null) {
if(args.isEmpty()) return
val type = args[0].toIntOrNull() ?: return
ACCOUNT_TYPE = type
usernameMatches[getCleanUserName().toLowerCase()] = ACCOUNT_TYPE
storeData()
}
}
}
}
fun OnKondoValueUpdated() {
storeData()
Init()
}
private fun cleanup() {
ACCOUNT_TYPE = 0
replace()
component = null
}
private fun replace() {
if (component != null && component?.id == TARGET_COMPONENT_ID) {
// Unmodified login name is used here to display it however the user types it
// caps and spaces respected.
val modifiedStr = replaceUsernameInBytes(component!!.text.chars, Player.usernameInput.toString())
component?.text = JagString.parse(modifiedStr)
}
}
private fun getCleanUserName(): String {
return Player.usernameInput.toString().replace(" ", "_")
}
private fun getConfig() {
val cleanUsername = getCleanUserName()
//println(cleanUsername)
// Abort if we are pre-login
if (cleanUsername.isEmpty()) return
// Check if we already have the account type for this user
val lowercaseUsername = cleanUsername.toLowerCase()
if (usernameMatches.containsKey(lowercaseUsername)) {
ACCOUNT_TYPE = usernameMatches[lowercaseUsername]!!
return
}
// No match found, check the hiscores (live server)
fetchAccountTypeFromAPI(lowercaseUsername)
}
private fun fetchAccountTypeFromAPI(lowercaseUsername : String){
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${lowercaseUsername}"
Thread {
try {
val url = URL(apiUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
// If a request take longer than 5 seconds timeout.
connection.connectTimeout = 5000
connection.readTimeout = 5000
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.use { it.readText() }
reader.close()
updatePlayerData(response, lowercaseUsername)
}
} catch (_: Exception) { }
}.start()
}
private fun updatePlayerData(jsonResponse: String, lowercaseUsername: String) {
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
ACCOUNT_TYPE = hiscoresResponse.info.iron_mode.toInt()
usernameMatches[lowercaseUsername] = ACCOUNT_TYPE
storeData()
}
private fun replaceUsernameInBytes(bytes: ByteArray, username: String): String {
// Convert bytes to string to work with them
val text = String(bytes, StandardCharsets.ISO_8859_1)
val colonIndex = text.indexOf(": ")
if (colonIndex != -1) {
val suffix = text.substring(colonIndex)
// modify and add the rest (username is always before the first ':' )
val newText = "${TYPE_ICONS.getOrDefault(ACCOUNT_TYPE, "")}$username$BUBBLE_ICON$suffix"
return encodeToJagStr(newText)
}
return encodeToJagStr(text)
}
private fun storeData() {
val jsonString = gson.toJson(usernameMatches)
API.StoreData(SETTINGS_KEY, jsonString)
}
private fun encodeToJagStr(str: String): String {
val result = StringBuilder()
for (char in str) {
val escaped = SPECIAL_ESCAPES[char]
if (escaped != null) {
result.append(escaped)
} else {
result.append(char)
}
}
return result.toString()
}
}

View file

@ -0,0 +1,5 @@
AUTHOR='downthecrop'
DESCRIPTION='Displays account type icons next to usernames in chat pulled from the live server API. \
Can be set with a chat command for single player with ::chathelm 1 (0=Normal, 1=IM, 2=HCIM, 3=UIM) \
or via KondoKit settings.'
VERSION=1.0

View file

@ -1,510 +0,0 @@
package KondoKit
import KondoKit.Constants.COLOR_BACKGROUND_DARK
import KondoKit.Constants.SKILL_DISPLAY_ORDER
import KondoKit.Constants.SKILL_SPRITE_DIMENSION
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.Helpers.getSpriteId
import KondoKit.Helpers.showToast
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.plugin.Companion.POPUP_FOREGROUND
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import com.google.gson.Gson
import plugin.api.API
import rt4.Sprites
import java.awt.*
import java.awt.datatransfer.DataFlavor
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import javax.swing.*
import javax.swing.border.MatteBorder
import kotlin.math.floor
object Constants {
// Sprite IDs
const val COMBAT_LVL_SPRITE = 168
const val IRONMAN_SPRITE = 4
const val MAG_SPRITE = 1423
const val LVL_BAR_SPRITE = 898
// Dimensions
val SEARCH_FIELD_DIMENSION = Dimension(230, 30)
val ICON_DIMENSION_SMALL = Dimension(12, 12)
val ICON_DIMENSION_LARGE = Dimension(30, 30)
val HISCORE_PANEL_DIMENSION = Dimension(230, 500)
val FILTER_PANEL_DIMENSION = Dimension(230, 30)
val SKILLS_PANEL_DIMENSION = Dimension(230, 290)
val TOTAL_COMBAT_PANEL_DIMENSION = Dimension(230, 30)
val SKILL_PANEL_DIMENSION = Dimension(76, 35)
val IMAGE_CANVAS_DIMENSION = Dimension(20, 20)
val SKILL_SPRITE_DIMENSION = Dimension(14, 14)
val NUMBER_LABEL_DIMENSION = Dimension(20, 20)
// Colors
val COLOR_BACKGROUND_DARK = WIDGET_COLOR
val COLOR_BACKGROUND_MEDIUM = VIEW_BACKGROUND_COLOR
val COLOR_FOREGROUND_LIGHT = POPUP_FOREGROUND
// Fonts
val FONT_ARIAL_PLAIN_14 = Font("Arial", Font.PLAIN, 14)
val FONT_ARIAL_PLAIN_12 = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val FONT_ARIAL_BOLD_12 = Font("Arial", Font.BOLD, 12)
val SKILL_DISPLAY_ORDER = arrayOf(0,3,14,2,16,13,1,15,10,4,17,7,5,12,11,6,9,8,20,18,19,22,21,23)
}
var text: String = ""
object HiscoresView {
const val VIEW_NAME = "HISCORE_SEARCH_VIEW"
var hiScoreView: JPanel? = null
class CustomSearchField(private val hiscoresPanel: JPanel) : Canvas() {
private var cursorVisible: Boolean = true
private val gson = Gson()
private val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.MAG_SPRITE))
private val imageCanvas = bufferedImageSprite.let {
ImageCanvas(it).apply {
preferredSize = Constants.ICON_DIMENSION_SMALL
size = preferredSize
minimumSize = preferredSize
maximumSize = preferredSize
fillColor = COLOR_BACKGROUND_DARK
}
}
init {
preferredSize = Constants.SEARCH_FIELD_DIMENSION
background = Constants.COLOR_BACKGROUND_DARK
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_PLAIN_14
minimumSize = preferredSize
maximumSize = preferredSize
addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
// Prevent null character from being typed on Ctrl+A & Ctrl+V
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
e.consume()
return
}
if (e.keyChar == '\b') {
if (text.isNotEmpty()) {
text = text.dropLast(1)
}
} else if (e.keyChar == '\n') {
searchPlayer(text)
} else {
text += e.keyChar
}
SwingUtilities.invokeLater {
repaint()
}
}
override fun keyPressed(e: KeyEvent) {
if (e.isControlDown) {
when (e.keyCode) {
KeyEvent.VK_A -> {
text = ""
repaint()
}
KeyEvent.VK_V -> {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
text += pasteText
SwingUtilities.invokeLater {
repaint()
}
}
}
}
}
})
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (e.x > width - 20 && e.y < 20) {
text = ""
SwingUtilities.invokeLater {
repaint()
}
}
}
})
Timer(500) {
cursorVisible = !cursorVisible
if(focusedView == VIEW_NAME)
SwingUtilities.invokeLater {
repaint()
}
}.start()
}
override fun paint(g: Graphics) {
super.paint(g)
g.color = foreground
g.font = font
val fm = g.fontMetrics
val cursorX = fm.stringWidth(text) + 30
imageCanvas.let { canvas ->
val imgG = g.create(5, 5, canvas.width, canvas.height)
canvas.paint(imgG)
imgG.dispose()
}
g.drawString(text, 30, 20)
if (cursorVisible && hasFocus()) {
g.drawLine(cursorX, 5, cursorX, 25)
}
if (text.isNotEmpty()) {
g.color = Color.RED
g.drawString("x", width - 20, 20)
}
}
fun searchPlayer(username: String) {
text = username.replace(" ", "_")
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${text.toLowerCase()}"
updateHiscoresView(null, "Searching...")
Thread {
try {
val url = URL(apiUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
// If a request take longer than 5 seconds timeout.
connection.connectTimeout = 5000
connection.readTimeout = 5000
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.use { it.readText() }
reader.close()
SwingUtilities.invokeLater {
updatePlayerData(response, username)
}
} else {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Player not found!", JOptionPane.ERROR_MESSAGE)
}
}
} catch (e: SocketTimeoutException) {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Request timed out", JOptionPane.ERROR_MESSAGE)
}
} catch (e: Exception) {
// Handle other errors
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Error fetching data!", JOptionPane.ERROR_MESSAGE)
}
}
}.start()
}
private fun updatePlayerData(jsonResponse: String, username: String) {
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
updateHiscoresView(hiscoresResponse, username)
}
private fun updateHiscoresView(data: HiscoresResponse?, username: String) {
val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel
playerNameLabel?.removeAll() // Clear previous components
var nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, "", primaryColor), JLabel.CENTER).apply {
font = Constants.FONT_ARIAL_BOLD_12
foreground = Constants.COLOR_FOREGROUND_LIGHT
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
}
playerNameLabel?.add(nameLabel)
playerNameLabel?.revalidate()
playerNameLabel?.repaint()
if(data == null) return
playerNameLabel?.removeAll()
val ironMode = data.info.iron_mode
if (ironMode != "0") {
val ironmanBufferedImage = getBufferedImageFromSprite(Sprites.nameIcons[Constants.IRONMAN_SPRITE + ironMode.toInt() - 1])
val imageCanvas = ironmanBufferedImage.let {
ImageCanvas(it).apply {
preferredSize = Constants.IMAGE_CANVAS_DIMENSION
size = Constants.IMAGE_CANVAS_DIMENSION
}
}
playerNameLabel?.add(imageCanvas)
}
val exp_multiplier = data.info.exp_multiplier
nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, " (${exp_multiplier}x)", primaryColor), JLabel.CENTER).apply {
font = Constants.FONT_ARIAL_BOLD_12
foreground = Constants.COLOR_FOREGROUND_LIGHT
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
}
playerNameLabel?.add(nameLabel)
playerNameLabel?.revalidate()
playerNameLabel?.repaint()
// Update skill labels
data.skills.forEachIndexed { index, skill ->
val labelName = "skillLabel_$index"
val numberLabel = findComponentByName(hiscoresPanel, labelName) as? JLabel
numberLabel?.text = skill.static
}
updateTotalAndCombatLevel(data.skills, true)
hiscoresPanel.revalidate()
hiscoresPanel.repaint()
}
private fun updateTotalAndCombatLevel(skills: List<Skill>, isMemberWorld: Boolean) {
val totalLevel = skills.sumBy { it.static.toInt() }
val totalLevelLabel = findComponentByName(hiscoresPanel, "totalLevelLabel") as? JLabel
totalLevelLabel?.text = totalLevel.toString()
val attack = skills.find { it.id == "0" }?.static?.toInt() ?: 1
val defence = skills.find { it.id == "1" }?.static?.toInt() ?: 1
val strength = skills.find { it.id == "2" }?.static?.toInt() ?: 1
val hitpoints = skills.find { it.id == "3" }?.static?.toInt() ?: 1
val ranged = skills.find { it.id == "4" }?.static?.toInt() ?: 1
val prayer = skills.find { it.id == "5" }?.static?.toInt() ?: 1
val magic = skills.find { it.id == "6" }?.static?.toInt() ?: 1
val summoning = skills.find { it.id == "23" }?.static?.toInt() ?: 1
val combatLevel = calculateCombatLevel(attack, defence, strength, hitpoints, prayer, ranged, magic, summoning, true)
val combatLevelLabel = findComponentByName(hiscoresPanel, "combatLevelLabel") as? JLabel
combatLevelLabel?.text = combatLevel.toString()
}
private fun calculateCombatLevel(
attack: Int,
defence: Int,
strength: Int,
hitpoints: Int,
prayer: Int,
ranged: Int,
magic: Int,
summoning: Int,
isMemberWorld: Boolean
): Double {
val base = (defence + hitpoints + floor(prayer.toDouble() / 2)) * 0.25
val melee = (attack + strength) * 0.325
val range = floor(ranged * 1.5) * 0.325
val mage = floor(magic * 1.5) * 0.325
val maxCombatType = maxOf(melee, range, mage)
val summoningFactor = if (isMemberWorld) floor(summoning.toDouble() / 8) else 0.0
return Math.round((base + maxCombatType + summoningFactor) * 1000.0) / 1000.0
}
private fun findComponentByName(container: Container, name: String): Component? {
for (component in container.components) {
if (name == component.name) {
return component
}
if (component is Container) {
val child = findComponentByName(component, name)
if (child != null) {
return child
}
}
}
return null
}
}
fun createHiscoreSearchView() {
val hiscorePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
name = VIEW_NAME
background = Constants.COLOR_BACKGROUND_MEDIUM
preferredSize = Constants.HISCORE_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
}
val customSearchField = CustomSearchField(hiscorePanel)
val searchFieldWrapper = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
background = Constants.COLOR_BACKGROUND_MEDIUM
preferredSize = Constants.SEARCH_FIELD_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
alignmentX = Component.CENTER_ALIGNMENT
add(customSearchField)
}
val searchPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = Constants.COLOR_BACKGROUND_MEDIUM
add(searchFieldWrapper)
}
hiscorePanel.add(Box.createVerticalStrut(10))
hiscorePanel.add(searchPanel)
hiscorePanel.add(Box.createVerticalStrut(10))
val playerNamePanel = JPanel().apply {
layout = GridBagLayout() // This will center the JLabel both vertically and horizontally
background = TOOLTIP_BACKGROUND.darker()
preferredSize = Constants.FILTER_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
name = "playerNameLabel"
}
hiscorePanel.add(playerNamePanel)
hiscorePanel.add(Box.createVerticalStrut(10))
val skillsPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
background = Constants.COLOR_BACKGROUND_MEDIUM
preferredSize = Constants.SKILLS_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
}
for (i in SKILL_DISPLAY_ORDER) {
val skillPanel = JPanel().apply {
layout = BorderLayout()
background = COLOR_BACKGROUND_DARK
preferredSize = Constants.SKILL_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
border = MatteBorder(5, 0, 0, 0, COLOR_BACKGROUND_DARK)
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(i)))
val imageCanvas = bufferedImageSprite.let {
ImageCanvas(it).apply {
preferredSize = SKILL_SPRITE_DIMENSION
size = SKILL_SPRITE_DIMENSION
fillColor = COLOR_BACKGROUND_DARK
}
}
val numberLabel = JLabel("", JLabel.RIGHT).apply {
name = "skillLabel_$i"
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_PLAIN_12
preferredSize = Constants.NUMBER_LABEL_DIMENSION
minimumSize = Constants.NUMBER_LABEL_DIMENSION
}
val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply {
background = COLOR_BACKGROUND_DARK
add(imageCanvas)
add(numberLabel)
}
skillPanel.add(imageContainer, BorderLayout.CENTER)
skillsPanel.add(skillPanel)
}
hiscorePanel.add(skillsPanel)
val totalCombatPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
background = COLOR_BACKGROUND_DARK
preferredSize = Constants.TOTAL_COMBAT_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE))
val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply {
fillColor = COLOR_BACKGROUND_DARK
preferredSize = Constants.ICON_DIMENSION_LARGE
size = Constants.ICON_DIMENSION_LARGE
}
val totalLevelLabel = JLabel("").apply {
name = "totalLevelLabel"
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_BOLD_12
horizontalAlignment = JLabel.LEFT
iconTextGap = 10
}
val totalLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
background = Constants.COLOR_BACKGROUND_DARK
add(totalLevelIcon)
add(totalLevelLabel)
}
val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE))
val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply {
fillColor = COLOR_BACKGROUND_DARK
preferredSize = Constants.ICON_DIMENSION_LARGE
size = Constants.ICON_DIMENSION_LARGE
}
val combatLevelLabel = JLabel("").apply {
name = "combatLevelLabel"
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_BOLD_12
horizontalAlignment = JLabel.LEFT
iconTextGap = 10
}
val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
background = COLOR_BACKGROUND_DARK
add(combatLevelIcon)
add(combatLevelLabel)
}
totalCombatPanel.add(totalLevelPanel)
totalCombatPanel.add(combatLevelPanel)
hiscorePanel.add(totalCombatPanel)
hiscorePanel.add(Box.createVerticalStrut(10))
hiScoreView = hiscorePanel
}
data class HiscoresResponse(
val info: PlayerInfo,
val skills: List<Skill>
)
data class PlayerInfo(
val exp_multiplier: String,
val iron_mode: String
)
data class Skill(
val id: String,
val dynamic: String,
val experience: String,
val static: String
)
}

View file

@ -1,324 +0,0 @@
package KondoKit
import KondoKit.Helpers.convertValue
import KondoKit.Helpers.showToast
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import plugin.Plugin
import plugin.PluginInfo
import plugin.PluginRepository
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.*
import java.util.Timer
import javax.swing.*
import kotlin.math.ceil
/*
This is used for the runtime editing of plugin variables.
To expose fields add the @Exposed annotation.
When they are applied this will trigger an invoke of OnKondoValueUpdated()
if it is implemented. Check GroundItems plugin for an example.
*/
object ReflectiveEditorView {
var reflectiveEditorView: JPanel? = null
private val loadedPlugins: MutableList<String> = mutableListOf()
const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW"
fun createReflectiveEditorView() {
val reflectiveEditorPanel = JPanel(BorderLayout())
reflectiveEditorPanel.background = VIEW_BACKGROUND_COLOR
reflectiveEditorPanel.add(Box.createVerticalStrut(5))
reflectiveEditorPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
reflectiveEditorView = reflectiveEditorPanel
addPlugins(reflectiveEditorView!!)
}
fun addPlugins(reflectiveEditorView: JPanel) {
reflectiveEditorView.removeAll() // clear previous
loadedPlugins.clear()
try {
val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins")
loadedPluginsField.isAccessible = true
val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *>
for ((pluginInfo, plugin) in loadedPlugins) {
addPluginToEditor(reflectiveEditorView, pluginInfo as PluginInfo, plugin as Plugin)
}
} catch (e: Exception) {
e.printStackTrace()
}
// Add a centered box for plugins that have no exposed fields
if (loadedPlugins.isNotEmpty()) {
val noExposedPanel = JPanel(BorderLayout())
noExposedPanel.background = VIEW_BACKGROUND_COLOR
noExposedPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
val label = JLabel("Loaded Plugins without Exposed Fields", SwingConstants.CENTER)
label.font = Font("RuneScape Small", Font.PLAIN, 16)
label.foreground = primaryColor
noExposedPanel.add(label, BorderLayout.NORTH)
val pluginsList = JList(loadedPlugins.toTypedArray())
pluginsList.background = WIDGET_COLOR
pluginsList.foreground = secondaryColor
pluginsList.font = Font("RuneScape Small", Font.PLAIN, 16)
// Wrap the JList in a JScrollPane with a fixed height
val maxScrollPaneHeight = 200
val scrollPane = JScrollPane(pluginsList).apply {
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
}
// Create a wrapper panel with BoxLayout to constrain the scroll pane
val scrollPaneWrapper = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
add(scrollPane)
}
noExposedPanel.add(scrollPaneWrapper, BorderLayout.CENTER)
// Center the panel within the reflectiveEditorView
val centeredPanel = JPanel().apply {
preferredSize = Dimension(240, maxScrollPaneHeight)
maximumSize = preferredSize
minimumSize = preferredSize
}
centeredPanel.layout = BoxLayout(centeredPanel, BoxLayout.Y_AXIS)
centeredPanel.add(Box.createVerticalGlue())
centeredPanel.add(noExposedPanel)
centeredPanel.add(Box.createVerticalGlue())
reflectiveEditorView.add(Box.createVerticalStrut(10))
reflectiveEditorView.add(centeredPanel)
}
reflectiveEditorView.revalidate()
if(focusedView == VIEW_NAME)
reflectiveEditorView.repaint()
}
private fun addPluginToEditor(reflectiveEditorView: JPanel, pluginInfo : PluginInfo, plugin: Plugin) {
reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS)
val fieldNotifier = Helpers.FieldNotifier(plugin)
val exposedFields = plugin.javaClass.declaredFields.filter { field ->
field.annotations.any { annotation ->
annotation.annotationClass.simpleName == "Exposed"
}
}
if (exposedFields.isNotEmpty()) {
val packageName = plugin.javaClass.`package`.name
val version = pluginInfo.version
val labelPanel = JPanel(BorderLayout())
labelPanel.maximumSize = Dimension(Int.MAX_VALUE, 30)
labelPanel.background = VIEW_BACKGROUND_COLOR
labelPanel.border = BorderFactory.createEmptyBorder(5, 0, 0, 0)
val label = JLabel("$packageName v$version", SwingConstants.CENTER)
label.foreground = primaryColor
label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
labelPanel.add(label, BorderLayout.CENTER)
label.isOpaque = true
label.background = TITLE_BAR_COLOR
reflectiveEditorView.add(labelPanel)
for (field in exposedFields) {
field.isAccessible = true
// Get the "Exposed" annotation specifically and retrieve its description, if available
val exposedAnnotation = field.annotations.firstOrNull { annotation ->
annotation.annotationClass.simpleName == "Exposed"
}
val description = exposedAnnotation?.let { annotation ->
try {
val descriptionField = annotation.annotationClass.java.getMethod("description")
descriptionField.invoke(annotation) as String
} catch (e: NoSuchMethodException) {
"" // No description method, return empty string
}
} ?: ""
val fieldPanel = JPanel()
fieldPanel.layout = GridBagLayout()
fieldPanel.background = WIDGET_COLOR
fieldPanel.foreground = secondaryColor
fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 40)
val gbc = GridBagConstraints()
gbc.insets = Insets(0, 5, 0, 5)
val label = JLabel(field.name.capitalize())
label.foreground = secondaryColor
gbc.gridx = 0
gbc.gridy = 0
gbc.weightx = 0.0
label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
gbc.anchor = GridBagConstraints.WEST
fieldPanel.add(label, gbc)
// Create appropriate input component based on field type
val inputComponent: JComponent = when {
field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> JCheckBox().apply {
isSelected = field.get(plugin) as Boolean
}
field.type.isEnum -> JComboBox((field.type.enumConstants as Array<Enum<*>>)).apply {
selectedItem = field.get(plugin)
}
field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1))
field.type == Float::class.javaPrimitiveType || field.type == Double::class.javaPrimitiveType || field.type == java.lang.Float::class.java || field.type == java.lang.Double::class.java -> JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1))
else -> JTextField(field.get(plugin)?.toString() ?: "")
}
// Add mouse listener to the label only if a description is available
if (description.isNotBlank()) {
label.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
label.addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
showCustomToolTip(description, label)
}
override fun mouseExited(e: MouseEvent) {
customToolTipWindow?.isVisible = false
}
})
}
gbc.gridx = 1
gbc.gridy = 0
gbc.weightx = 1.0
gbc.fill = GridBagConstraints.HORIZONTAL
fieldPanel.add(inputComponent, gbc)
val applyButton = JButton("\u2714").apply {
maximumSize = Dimension(Int.MAX_VALUE, 8)
}
gbc.gridx = 2
gbc.gridy = 0
gbc.weightx = 0.0
gbc.fill = GridBagConstraints.NONE
applyButton.addActionListener {
try {
val newValue = when (inputComponent) {
is JCheckBox -> inputComponent.isSelected
is JComboBox<*> -> inputComponent.selectedItem
is JSpinner -> inputComponent.value
is JTextField -> convertValue(field.type, field.genericType, inputComponent.text)
else -> throw IllegalArgumentException("Unsupported input component type")
}
fieldNotifier.setFieldValue(field, newValue)
showToast(
reflectiveEditorView,
"${field.name} updated successfully!"
)
} catch (e: Exception) {
showToast(
reflectiveEditorView,
"Failed to update ${field.name}: ${e.message}",
JOptionPane.ERROR_MESSAGE
)
}
}
fieldPanel.add(applyButton, gbc)
reflectiveEditorView.add(fieldPanel)
// Track field changes in real-time and update UI
var previousValue = field.get(plugin)?.toString()
val timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
val currentValue = field.get(plugin)?.toString()
if (currentValue != previousValue) {
previousValue = currentValue
SwingUtilities.invokeLater {
// Update the inputComponent based on the new value
when (inputComponent) {
is JCheckBox -> inputComponent.isSelected = field.get(plugin) as Boolean
is JComboBox<*> -> inputComponent.selectedItem = field.get(plugin)
is JSpinner -> inputComponent.value = field.get(plugin)
is JTextField -> inputComponent.text = field.get(plugin)?.toString() ?: ""
}
}
}
}
}, 0, 1000) // Poll every 1000 milliseconds (1 second)
}
if (exposedFields.isNotEmpty()) {
reflectiveEditorView.add(Box.createVerticalStrut(5))
}
}
else {
loadedPlugins.add(plugin.javaClass.`package`.name)
}
}
var customToolTipWindow: JWindow? = null
fun showCustomToolTip(text: String, component: JComponent) {
val _font = Font("RuneScape Small", Font.PLAIN, 16)
val maxWidth = 150
val lineHeight = 16
// Create a dummy JLabel to get FontMetrics for the font used in the tooltip
val dummyLabel = JLabel()
dummyLabel.font = _font
val fontMetrics = dummyLabel.getFontMetrics(_font)
// Calculate the approximate width of the text
val textWidth = fontMetrics.stringWidth(text)
// Calculate the number of lines required based on the text width and max tooltip width
val numberOfLines = ceil(textWidth.toDouble() / maxWidth).toInt()
// Calculate the required height of the tooltip
val requiredHeight = numberOfLines * lineHeight + 6 // Adding some padding
if (customToolTipWindow == null) {
customToolTipWindow = JWindow().apply {
val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND)
val textColor = Helpers.colorToHex(secondaryColor)
contentPane = JLabel("<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>").apply {
border = BorderFactory.createLineBorder(Color.BLACK)
isOpaque = true
background = TOOLTIP_BACKGROUND
foreground = Color.WHITE
font = _font
maximumSize = Dimension(maxWidth, Int.MAX_VALUE)
preferredSize = Dimension(maxWidth, requiredHeight)
}
pack()
}
} else {
// Update the tooltip text
val label = customToolTipWindow!!.contentPane as JLabel
val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND)
val textColor = Helpers.colorToHex(secondaryColor)
label.text = "<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>"
label.preferredSize = Dimension(maxWidth, requiredHeight)
customToolTipWindow!!.pack()
}
// Position the tooltip near the component
val locationOnScreen = component.locationOnScreen
customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15)
customToolTipWindow!!.isVisible = true
}
}

View file

@ -1,66 +0,0 @@
package KondoKit
import plugin.api.API
import rt4.IntNode
import rt4.Node
object XPTable {
private const val MAX_LEVEL = 99
private const val INVALID_LEVEL = -1
private const val SKILLS_XP_TABLE = 716
private var xpTable: MutableList<Int> = mutableListOf()
// Lazily load the XP table from the API if it's empty
private fun loadXpTable() {
if (xpTable.isEmpty()) {
// Add the initial entry for key 1 = 0
xpTable.add(0)
// Fetch XP table from the API
API.GetDataMap(SKILLS_XP_TABLE).table.nodes.forEach { bucket ->
var currentNode: Node = bucket.nextNode
while (currentNode !== bucket) {
if (currentNode is IntNode) {
xpTable.add(currentNode.value)
}
currentNode = currentNode.nextNode
}
}
}
}
fun getXpRequiredForLevel(level: Int): Int {
loadXpTable()
if (level in 1..xpTable.size) {
return xpTable[level - 1]
}
return 0
}
fun getLevelForXp(xp: Int): Pair<Int, Int> {
loadXpTable()
var lowIndex = 0
var highIndex = xpTable.size - 1
if (xp >= xpTable[highIndex]) {
return Pair(MAX_LEVEL, xp - xpTable[highIndex]) // Level is max or above, return the highest level
}
while (lowIndex <= highIndex) {
val midIndex = (lowIndex + highIndex) / 2
when {
xp < xpTable[midIndex] -> highIndex = midIndex - 1
xp >= xpTable[midIndex + 1] -> lowIndex = midIndex + 1
else -> {
val currentLevel = midIndex + 1
val xpGained = xp - xpTable[midIndex]
return Pair(currentLevel, xpGained)
}
}
}
return Pair(INVALID_LEVEL, 0) // If xp is below all defined levels
}
}

View file

@ -1,444 +0,0 @@
package KondoKit
import KondoKit.Helpers.addMouseListenerToAll
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.Helpers.formatNumber
import KondoKit.Helpers.getProgressBarColor
import KondoKit.Helpers.getSpriteId
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.plugin.Companion.IMAGE_SIZE
import KondoKit.plugin.Companion.LVL_ICON
import KondoKit.plugin.Companion.POPUP_BACKGROUND
import KondoKit.plugin.Companion.POPUP_FOREGROUND
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.WIDGET_SIZE
import KondoKit.plugin.Companion.playerXPMultiplier
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import plugin.api.API
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import javax.swing.*
object XPTrackerView {
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4)
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
var totalXPWidget: XPWidget? = null
val initialXP: MutableMap<Int, Int> = HashMap()
var xpTrackerView: JPanel? = null
const val VIEW_NAME = "XP_TRACKER_VIEW"
val npcHitpointsMap: Map<Int, Int> = 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<Int, Int>()
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
}
} catch (e: Exception) {
println("XPTracker Error parsing NPC HP: ${e.message}")
emptyMap()
}
fun updateWidget(xpWidget: XPWidget, xp: Int) {
val (currentLevel, xpGainedSinceLastLevel) = XPTable.getLevelForXp(xp)
var xpGainedSinceLastUpdate = xp - xpWidget.previousXp
xpWidget.totalXpGained += xpGainedSinceLastUpdate
updateTotalXPWidget(xpGainedSinceLastUpdate)
val progress: Double
if (currentLevel >= 99) {
progress = 100.0 // Set progress to 100% if the level is 99 or above
xpWidget.xpLeftLabel.text = "" // Hide XP Left when level is 99
xpWidget.actionsRemainingLabel.text = ""
} else {
val nextLevelXp = XPTable.getXpRequiredForLevel(currentLevel + 1)
val xpLeft = nextLevelXp - xp
progress = xpGainedSinceLastLevel.toDouble() / (nextLevelXp - XPTable.getXpRequiredForLevel(currentLevel)) * 100
val xpLeftstr = formatNumber(xpLeft)
xpWidget.xpLeftLabel.text = formatHtmlLabelText("XP Left: ", primaryColor, xpLeftstr, secondaryColor)
if(COMBAT_SKILLS.contains(xpWidget.skillId)) {
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
val xpPerKill = when (xpWidget.skillId) {
3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints
else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
}
val remainingKills = xpLeft / xpPerKill
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
}
} else {
if(xpGainedSinceLastUpdate == 0)
xpGainedSinceLastUpdate = 1 // Avoid possible divide by 0
val remainingActions = (xpLeft / xpGainedSinceLastUpdate).coerceAtLeast(1)
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Actions: ", primaryColor, remainingActions.toString(), secondaryColor)
}
}
val formattedXp = formatNumber(xpWidget.totalXpGained)
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
// Update the progress bar with current level, progress, and next level
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME)
xpWidget.previousXp = xp
if (focusedView == VIEW_NAME)
xpWidget.container.repaint()
}
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
val totalXPWidget = totalXPWidget ?: return
totalXPWidget.totalXpGained += xpGainedSinceLastUpdate
val formattedXp = formatNumber(totalXPWidget.totalXpGained)
totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor)
if (focusedView == VIEW_NAME)
totalXPWidget.container.repaint()
}
fun resetXPTracker(xpTrackerView : JPanel){
// Redo logic here
xpTrackerView.removeAll()
val popupMenu = createResetMenu()
// Create a custom MouseListener
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)
}
}
}
// Create the XP widget
totalXPWidget = createTotalXPWidget()
val wrapped = wrappedWidget(totalXPWidget!!.container)
addMouseListenerToAll(wrapped,rightClickListener)
wrapped.addMouseListener(rightClickListener)
xpTrackerView.add(Box.createVerticalStrut(5))
xpTrackerView.add(wrapped)
xpTrackerView.add(Box.createVerticalStrut(5))
initialXP.clear()
xpWidgets.clear()
xpTrackerView.revalidate()
if (focusedView == VIEW_NAME)
xpTrackerView.repaint()
}
fun createTotalXPWidget(): XPWidget {
val widgetPanel = Panel().apply {
layout = BorderLayout(5, 5)
background = WIDGET_COLOR
preferredSize = TOTAL_XP_WIDGET_SIZE
maximumSize = TOTAL_XP_WIDGET_SIZE
minimumSize = TOTAL_XP_WIDGET_SIZE
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(LVL_ICON))
val imageContainer = Panel(FlowLayout()).apply {
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
maximumSize = preferredSize
minimumSize = preferredSize
size = preferredSize
}
bufferedImageSprite.let { image ->
val imageCanvas = ImageCanvas(image).apply {
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
maximumSize = preferredSize
minimumSize = preferredSize
size = preferredSize
}
imageContainer.add(imageCanvas)
imageContainer.size = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
imageContainer.revalidate()
if(focusedView == VIEW_NAME)
imageContainer.repaint()
}
val textPanel = Panel().apply {
layout = GridLayout(2, 1, 5, 0)
}
val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val xpGainedLabel = JLabel(
formatHtmlLabelText("Gained: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val xpPerHourLabel = JLabel(
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
textPanel.add(xpGainedLabel)
textPanel.add(xpPerHourLabel)
widgetPanel.add(imageContainer, BorderLayout.WEST)
widgetPanel.add(textPanel, BorderLayout.CENTER)
return XPWidget(
skillId = -1,
container = widgetPanel,
xpGainedLabel = xpGainedLabel,
xpLeftLabel = JLabel(formatHtmlLabelText("XP Left: ", primaryColor, "0", secondaryColor)).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
},
xpPerHourLabel = xpPerHourLabel,
progressBar = ProgressBar(0.0, Color.BLACK), // Unused
totalXpGained = 0,
startTime = System.currentTimeMillis(),
previousXp = 0,
actionsRemainingLabel = JLabel(),
)
}
fun createXPTrackerView(){
val widgetViewPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = VIEW_BACKGROUND_COLOR
}
val popupMenu = createResetMenu()
// Create a custom MouseListener
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)
}
}
}
// Create the XP widget
totalXPWidget = createTotalXPWidget()
val wrapped = wrappedWidget(totalXPWidget!!.container)
addMouseListenerToAll(wrapped,rightClickListener)
wrapped.addMouseListener(rightClickListener)
widgetViewPanel.add(Box.createVerticalStrut(5))
widgetViewPanel.add(wrapped)
widgetViewPanel.add(Box.createVerticalStrut(5))
xpTrackerView = widgetViewPanel
}
fun createResetMenu(): JPopupMenu {
// Create a popup menu
val popupMenu = JPopupMenu()
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
popupMenu.background = POPUP_BACKGROUND
// Create menu items with custom font and colors
val menuItem1 = JMenuItem("Reset Tracker").apply {
font = rFont // Set custom font
background = POPUP_BACKGROUND // Dark background for item
foreground = POPUP_FOREGROUND // Light text color for item
}
// Add menu items to the popup menu
popupMenu.add(menuItem1)
// Add action listeners to each menu item (optional)
menuItem1.addActionListener { plugin.registerDrawAction { resetXPTracker(xpTrackerView!!) } }
return popupMenu
}
fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
val widgetPanel = Panel().apply {
layout = BorderLayout(5, 5)
background = WIDGET_COLOR
preferredSize = WIDGET_SIZE
maximumSize = WIDGET_SIZE
minimumSize = WIDGET_SIZE
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId)))
val imageContainer = Panel(FlowLayout()).apply {
background = WIDGET_COLOR
preferredSize = IMAGE_SIZE
maximumSize = IMAGE_SIZE
minimumSize = IMAGE_SIZE
size = IMAGE_SIZE
}
bufferedImageSprite.let { image ->
val imageCanvas = ImageCanvas(image).apply {
background = WIDGET_COLOR
preferredSize = Dimension(image.width, image.height)
maximumSize = Dimension(image.width, image.height)
minimumSize = Dimension(image.width, image.height)
size = Dimension(image.width, image.height) // Explicitly set the size
}
imageContainer.add(imageCanvas)
imageContainer.size = Dimension(image.width, image.height) // Ensure container respects the image size
imageContainer.revalidate()
if(focusedView == VIEW_NAME)
imageContainer.repaint()
}
val textPanel = Panel().apply {
layout = GridLayout(2, 2, 5, 0)
background = WIDGET_COLOR
}
val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val xpGainedLabel = JLabel(
formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val xpLeftLabel = JLabel(
formatHtmlLabelText("XP Left: ", primaryColor, "0K", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val xpPerHourLabel = JLabel(
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val killsLabel = JLabel(
formatHtmlLabelText("Kills: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val levelPanel = Panel().apply {
layout = BorderLayout(5, 0)
background = WIDGET_COLOR
}
val progressBarPanel = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
preferredSize = Dimension(160, 22)
}
levelPanel.add(progressBarPanel, BorderLayout.CENTER)
textPanel.add(xpGainedLabel)
textPanel.add(xpLeftLabel)
textPanel.add(xpPerHourLabel)
textPanel.add(killsLabel)
widgetPanel.add(imageContainer, BorderLayout.WEST)
widgetPanel.add(textPanel, BorderLayout.CENTER)
widgetPanel.add(levelPanel, BorderLayout.SOUTH)
widgetPanel.revalidate()
if(focusedView == VIEW_NAME)
widgetPanel.repaint()
return XPWidget(
skillId = skillId,
container = widgetPanel,
xpGainedLabel = xpGainedLabel,
xpLeftLabel = xpLeftLabel,
xpPerHourLabel = xpPerHourLabel,
progressBar = progressBarPanel,
totalXpGained = 0,
actionsRemainingLabel = killsLabel,
startTime = System.currentTimeMillis(),
previousXp = previousXp
)
}
fun wrappedWidget(component: Component, padding: Int = 7): Container {
val outerPanelSize = Dimension(
component.preferredSize.width + 2 * padding,
component.preferredSize.height + 2 * padding
)
val outerPanel = JPanel(GridBagLayout()).apply {
background = WIDGET_COLOR
preferredSize = outerPanelSize
maximumSize = outerPanelSize
minimumSize = outerPanelSize
}
val innerPanel = JPanel(BorderLayout()).apply {
background = WIDGET_COLOR
preferredSize = component.preferredSize
maximumSize = component.preferredSize
minimumSize = component.preferredSize
add(component, BorderLayout.CENTER)
}
val gbc = GridBagConstraints().apply {
anchor = GridBagConstraints.CENTER
}
outerPanel.add(innerPanel, gbc)
return outerPanel
}
}
data class XPWidget(
val container: Container,
val skillId: Int,
val xpGainedLabel: JLabel,
val xpLeftLabel: JLabel,
val xpPerHourLabel: JLabel,
val actionsRemainingLabel: JLabel,
val progressBar: ProgressBar,
var totalXpGained: Int = 0,
var startTime: Long = System.currentTimeMillis(),
var previousXp: Int = 0
)

View file

@ -1,35 +1,22 @@
package KondoKit
import KondoKit.Constants.COMBAT_LVL_SPRITE
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.Helpers.formatNumber
import KondoKit.Helpers.getSpriteId
import KondoKit.Helpers.showAlert
import KondoKit.HiscoresView.createHiscoreSearchView
import KondoKit.HiscoresView.hiScoreView
import KondoKit.LootTrackerView.BAG_ICON
import KondoKit.LootTrackerView.createLootTrackerView
import KondoKit.LootTrackerView.lootTrackerView
import KondoKit.LootTrackerView.npcDeathSnapshots
import KondoKit.LootTrackerView.onPostClientTick
import KondoKit.LootTrackerView.takeGroundSnapshot
import KondoKit.ReflectiveEditorView.addPlugins
import KondoKit.ReflectiveEditorView.createReflectiveEditorView
import KondoKit.ReflectiveEditorView.reflectiveEditorView
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.Themes.Theme
import KondoKit.Themes.ThemeType
import KondoKit.Themes.getTheme
import KondoKit.XPTrackerView.createXPTrackerView
import KondoKit.XPTrackerView.createXPWidget
import KondoKit.XPTrackerView.initialXP
import KondoKit.XPTrackerView.resetXPTracker
import KondoKit.XPTrackerView.totalXPWidget
import KondoKit.XPTrackerView.updateWidget
import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.XPTrackerView.xpTrackerView
import KondoKit.XPTrackerView.xpWidgets
import KondoKit.plugin.StateManager.focusedView
import KondoKit.util.Helpers.getSpriteId
import KondoKit.util.Helpers.showAlert
import KondoKit.views.*
import KondoKit.ui.OnUpdateCallback
import KondoKit.ui.OnDrawCallback
import KondoKit.ui.OnXPUpdateCallback
import KondoKit.ui.OnKillingBlowNPCCallback
import KondoKit.ui.OnPostClientTickCallback
import KondoKit.util.AltCanvas
import KondoKit.util.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.util.ImageCanvas
import KondoKit.util.setFixedSize
import KondoKit.ui.ViewConstants
import KondoKit.ui.theme.Themes.Theme
import KondoKit.ui.theme.Themes.ThemeType
import KondoKit.ui.theme.Themes.getTheme
import KondoKit.ui.components.ScrollablePanel
import plugin.Plugin
import plugin.api.*
import plugin.api.API.*
@ -46,6 +33,8 @@ import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
import KondoKit.ui.View
import KondoKit.util.Helpers
@Target(AnnotationTarget.FIELD)
@ -54,8 +43,6 @@ annotation class Exposed(val description: String = "")
class plugin : Plugin() {
companion object {
val WIDGET_SIZE = Dimension(220, 50)
val TOTAL_XP_WIDGET_SIZE = Dimension(220, 30)
val IMAGE_SIZE = Dimension(25, 23)
// Default Theme Colors
@ -98,9 +85,9 @@ class plugin : Plugin() {
const val FIXED_HEIGHT = 503
private const val NAVBAR_WIDTH = 30
private const val MAIN_CONTENT_WIDTH = 242
private const val WRENCH_ICON = 907
private const val LOOT_ICON = 777
private const val MAG_SPRITE = 1423
const val WRENCH_ICON = 907
const val LOOT_ICON = 777
const val MAG_SPRITE = 1423
const val LVL_ICON = 898
private lateinit var cardLayout: CardLayout
private lateinit var mainContentPanel: JPanel
@ -118,16 +105,41 @@ class plugin : Plugin() {
private const val HIDDEN_VIEW = "HIDDEN"
private var altCanvas: AltCanvas? = null
private val drawActions = mutableListOf<() -> Unit>()
private val views = mutableListOf<View>()
private val updateCallbacks = mutableListOf<OnUpdateCallback>()
private val drawCallbacks = mutableListOf<OnDrawCallback>()
private val xpUpdateCallbacks = mutableListOf<OnXPUpdateCallback>()
private val killingBlowNPCCallbacks = mutableListOf<OnKillingBlowNPCCallback>()
private val postClientTickCallbacks = mutableListOf<OnPostClientTickCallback>()
fun registerDrawAction(action: () -> Unit) {
synchronized(drawActions) {
drawActions.add(action)
}
}
fun registerUpdateCallback(callback: OnUpdateCallback) {
updateCallbacks.add(callback)
}
fun registerDrawCallback(callback: OnDrawCallback) {
drawCallbacks.add(callback)
}
fun registerXPUpdateCallback(callback: OnXPUpdateCallback) {
xpUpdateCallbacks.add(callback)
}
fun registerKillingBlowNPCCallback(callback: OnKillingBlowNPCCallback) {
killingBlowNPCCallbacks.add(callback)
}
fun registerPostClientTickCallback(callback: OnPostClientTickCallback) {
postClientTickCallbacks.add(callback)
}
}
override fun Init() {
// Disable Font AA
System.setProperty("sun.java2d.opengl", "false")
System.setProperty("awt.useSystemAAFontSettings", "off")
System.setProperty("swing.aatext", "false")
@ -135,9 +147,7 @@ class plugin : Plugin() {
override fun OnLogin() {
if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) {
// if we logged in with a new character
// we need to reset the trackers
xpTrackerView?.let { resetXPTracker(it) }
XPTrackerView.xpTrackerView?.let { XPTrackerView.resetXPTracker(it) }
}
lastLogin = Player.usernameInput.toString()
}
@ -147,15 +157,12 @@ class plugin : Plugin() {
for ((index, entry) in currentEntries.withIndex()) {
if (entry.type == MiniMenuType.PLAYER && index == currentEntries.size - 1) {
val input = entry.subject
// Trim spaces, clean up tags, and remove the level info
val cleanedInput = input
.trim() // Remove any leading/trailing spaces
.replace(Regex("<col=[0-9a-fA-F]{6}>"), "") // Remove color tags
.replace(Regex("<img=\\d+>"), "") // Remove image tags
.replace(Regex("\\(level: \\d+\\)"), "") // Remove level text e.g. (level: 44)
.trim() // Trim again to remove extra spaces after removing level text
// Proceed with the full cleaned username
.trim()
.replace(Regex("<col=[0-9a-fA-F]{6}>"), "")
.replace(Regex("<img=\\d+>"), "")
.replace(Regex("\\(level: \\d+\\)"), "")
.trim()
InsertMiniMenuEntry("Lookup", entry.subject, searchHiscore(cleanedInput))
}
}
@ -164,41 +171,39 @@ 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()
rightPanelWrapper?.let { wrapper ->
wrapper.ignoreRepaint = true
try {
val parent = wrapper.parent
val wrapperNeedsAttach = parent != frame
if (wrapperNeedsAttach) {
parent?.remove(wrapper)
frame.layout = BorderLayout()
frame.add(wrapper, BorderLayout.EAST)
}
wrapper.revalidate()
wrapper.repaint()
} finally {
wrapper.ignoreRepaint = false
}
}
frame.revalidate()
frame.repaint()
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
pluginsReloaded = true
reloadInterfaces = true
return true
}
override fun OnXPUpdate(skillId: Int, xp: Int) {
if (!initialXP.containsKey(skillId)) {
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
xpWidget = createXPWidget(skillId, previousXp)
xpWidgets[skillId] = xpWidget
xpTrackerView?.add(wrappedWidget(xpWidget.container))
xpTrackerView?.add(Box.createVerticalStrut(5))
if(focusedView == XPTrackerView.VIEW_NAME) {
xpTrackerView?.revalidate()
xpTrackerView?.repaint()
}
updateWidget(xpWidget, xp)
}
xpUpdateCallbacks.forEach { callback ->
callback.onXPUpdate(skillId, xp)
}
}
override fun Draw(timeDelta: Long) {
@ -208,7 +213,9 @@ class plugin : Plugin() {
}
if (pluginsReloaded) {
reflectiveEditorView?.let { addPlugins(it) }
SwingUtilities.invokeLater {
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
pluginsReloaded = false
}
@ -219,10 +226,16 @@ class plugin : Plugin() {
accumulatedTime += timeDelta
if (accumulatedTime >= TICK_INTERVAL) {
lootTrackerView?.let { onPostClientTick(it) }
postClientTickCallbacks.forEach { callback ->
callback.onPostClientTick()
}
accumulatedTime = 0L
}
drawCallbacks.forEach { callback ->
callback.onDraw(timeDelta)
}
// Draw synced actions (that require to be done between glBegin and glEnd)
if (drawActions.isNotEmpty()) {
synchronized(drawActions) {
@ -256,47 +269,29 @@ class plugin : Plugin() {
} else {
moveCanvasToFront()
}
altCanvas?.updateGameImage() // Update the game image as needed
altCanvas?.updateGameImage()
}
override fun Update() {
val widgets = xpWidgets.values
val totalXP = totalXPWidget
widgets.forEach { xpWidget ->
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedXpPerHour = formatNumber(xpPerHour)
xpWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
xpWidget.container.repaint()
}
totalXP?.let { totalXPWidget ->
val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0
val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
totalXPWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
totalXPWidget.container.repaint()
updateCallbacks.forEach { callback ->
callback.onUpdate()
}
}
override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) {
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
npcDeathSnapshots[npcID] = LootTrackerView.GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
killingBlowNPCCallbacks.forEach { callback ->
callback.onKillingBlowNPC(npcID, x, z)
}
}
private fun allSpritesLoaded() : Boolean {
// Check all skill sprites
try{
for (i in 0 until 24) {
if(!js5Archive8.isFileReady(getSpriteId(i))){
return false
}
}
val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, COMBAT_LVL_SPRITE, BAG_ICON)
val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, ViewConstants.COMBAT_LVL_SPRITE, LootTrackerView.BAG_ICON)
for (icon in otherIcons) {
if(!js5Archive8.isFileReady(icon)){
return false
@ -309,47 +304,77 @@ class plugin : Plugin() {
}
private fun updateDisplaySettings() {
val mode = GetWindowMode()
val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH
lastUIOffset = uiOffset
val applyDisplaySettings = {
val mode = GetWindowMode()
val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH
lastUIOffset = uiOffset
if(mode != WindowMode.FIXED) {
destroyAltCanvas()
} else if (useScaledFixed && altCanvas == null) {
initAltCanvas()
}
when (mode) {
WindowMode.FIXED -> {
if (frame.width < FIXED_WIDTH + currentScrollPaneWidth + uiOffset) {
frame.setSize(FIXED_WIDTH + currentScrollPaneWidth + uiOffset, frame.height)
}
val difference = frame.width - (uiOffset + currentScrollPaneWidth)
if (useScaledFixed) {
GameShell.leftMargin = 0
val canvasWidth = difference + uiOffset / 2
val canvasHeight = frame.height - canvas.y // Restricting height to frame height
altCanvas?.size = Dimension(canvasWidth, canvasHeight)
altCanvas?.setLocation(0, canvas.y)
canvas.setLocation(0, canvas.y)
} else {
val difference = frame.width - (FIXED_WIDTH + uiOffset + currentScrollPaneWidth)
GameShell.leftMargin = difference / 2
// Ensure the scroll wrapper stays attached on the EAST edge even if the game resets the layout
rightPanelWrapper?.let { wrapper ->
val needsLayoutReset = frame.layout !is BorderLayout
val needsAttach = wrapper.parent != frame
if (needsLayoutReset || needsAttach) {
wrapper.parent?.remove(wrapper)
frame.layout = BorderLayout()
frame.add(wrapper, BorderLayout.EAST)
if (altCanvas != null) {
moveAltCanvasToFront()
} else {
moveCanvasToFront()
}
}
}
WindowMode.RESIZABLE -> {
GameShell.canvasWidth = frame.width - (currentScrollPaneWidth + uiOffset)
if(mode != WindowMode.FIXED) {
destroyAltCanvas()
} else if (useScaledFixed && altCanvas == null) {
initAltCanvas()
} else if (!useScaledFixed && altCanvas != null) {
// Was using scaled fixed but toggled the setting
// restore the original canvas
moveCanvasToFront()
destroyAltCanvas()
}
when (mode) {
WindowMode.FIXED -> {
if (frame.width < FIXED_WIDTH + currentScrollPaneWidth + uiOffset) {
frame.setSize(FIXED_WIDTH + currentScrollPaneWidth + uiOffset, frame.height)
}
val difference = frame.width - (uiOffset + currentScrollPaneWidth)
if (useScaledFixed) {
GameShell.leftMargin = 0
val canvasWidth = difference + uiOffset / 2
val canvasHeight = frame.height - canvas.y // Restricting height to frame height
altCanvas?.size = Dimension(canvasWidth, canvasHeight)
altCanvas?.setLocation(0, canvas.y)
canvas.setLocation(0, canvas.y)
} else {
val difference = frame.width - (FIXED_WIDTH + uiOffset + currentScrollPaneWidth)
GameShell.leftMargin = difference / 2
}
}
WindowMode.RESIZABLE -> {
GameShell.canvasWidth = frame.width - (currentScrollPaneWidth + uiOffset)
}
}
rightPanelWrapper?.preferredSize = Dimension(currentScrollPaneWidth, frame.height)
rightPanelWrapper?.isDoubleBuffered = true
rightPanelWrapper?.revalidate()
rightPanelWrapper?.repaint()
frame.validate()
}
rightPanelWrapper?.preferredSize = Dimension(currentScrollPaneWidth, frame.height)
rightPanelWrapper?.isDoubleBuffered = true
rightPanelWrapper?.revalidate()
rightPanelWrapper?.repaint()
if (SwingUtilities.isEventDispatchThread()) {
applyDisplaySettings()
} else {
SwingUtilities.invokeLater { applyDisplaySettings() }
}
}
fun OnKondoValueUpdated(){
@ -409,10 +434,10 @@ class plugin : Plugin() {
private fun searchHiscore(username: String): Runnable {
return Runnable {
setActiveView(HiscoresView.VIEW_NAME)
val customSearchField = hiScoreView?.let { HiscoresView.CustomSearchField(it) }
customSearchField?.searchPlayer(username) ?: run {
println("searchView is null or CustomSearchField creation failed.")
HiscoresView.hiScoreView?.let { hiscoresPanel ->
HiscoresView.searchPlayerForHiscores(username, hiscoresPanel)
} ?: run {
println("hiscoresPanel is null")
}
}
}
@ -440,22 +465,36 @@ class plugin : Plugin() {
cardLayout = CardLayout()
mainContentPanel = JPanel(cardLayout).apply {
border = BorderFactory.createEmptyBorder(0, 0, 0, 0) // Removes any default border or padding
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
background = VIEW_BACKGROUND_COLOR
preferredSize = Dimension(MAIN_CONTENT_WIDTH, frame.height)
isOpaque = true
}
// Register Views
createXPTrackerView()
createHiscoreSearchView()
createLootTrackerView()
createReflectiveEditorView()
val xpTrackerView = XPTrackerView
val hiscoresView = HiscoresView
val lootTrackerView = LootTrackerView
val reflectiveEditorView = ReflectiveEditorView
xpTrackerView.createView()
hiscoresView.createView()
lootTrackerView.createView()
reflectiveEditorView.createView()
views.add(xpTrackerView)
views.add(hiscoresView)
views.add(lootTrackerView)
views.add(reflectiveEditorView)
xpTrackerView.registerFunctions()
hiscoresView.registerFunctions()
lootTrackerView.registerFunctions()
reflectiveEditorView.registerFunctions()
mainContentPanel.add(ScrollablePanel(xpTrackerView!!), XPTrackerView.VIEW_NAME)
mainContentPanel.add(ScrollablePanel(hiScoreView!!), HiscoresView.VIEW_NAME)
mainContentPanel.add(ScrollablePanel(lootTrackerView!!), LootTrackerView.VIEW_NAME)
mainContentPanel.add(ScrollablePanel(reflectiveEditorView!!), ReflectiveEditorView.VIEW_NAME)
mainContentPanel.add(ScrollablePanel(xpTrackerView.panel), xpTrackerView.name)
mainContentPanel.add(ScrollablePanel(hiscoresView.panel), hiscoresView.name)
mainContentPanel.add(ScrollablePanel(lootTrackerView.panel), lootTrackerView.name)
mainContentPanel.add(ScrollablePanel(reflectiveEditorView.panel), reflectiveEditorView.name)
val navPanel = Panel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
@ -463,10 +502,10 @@ class plugin : Plugin() {
preferredSize = Dimension(NAVBAR_WIDTH, frame.height)
}
navPanel.add(createNavButton(LVL_ICON, XPTrackerView.VIEW_NAME))
navPanel.add(createNavButton(MAG_SPRITE, HiscoresView.VIEW_NAME))
navPanel.add(createNavButton(LOOT_ICON, LootTrackerView.VIEW_NAME))
navPanel.add(createNavButton(WRENCH_ICON, ReflectiveEditorView.VIEW_NAME))
navPanel.add(createNavButton(xpTrackerView.iconSpriteId, xpTrackerView.name))
navPanel.add(createNavButton(hiscoresView.iconSpriteId, hiscoresView.name))
navPanel.add(createNavButton(lootTrackerView.iconSpriteId, lootTrackerView.name))
navPanel.add(createNavButton(reflectiveEditorView.iconSpriteId, reflectiveEditorView.name))
val rightPanel = Panel(BorderLayout()).apply {
add(mainContentPanel, BorderLayout.CENTER)
@ -481,45 +520,80 @@ 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.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 {
SwingUtilities.invokeAndWait(commit)
} catch (e: Exception) {
// Fallback to async if invokeAndWait fails for any reason
SwingUtilities.invokeLater(commit)
}
}
initialized = true
pluginsReloaded = true
updateDisplaySettings()
}
}
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()
}
StateManager.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 {
@ -535,36 +609,29 @@ class plugin : Plugin() {
}
lastClickTime = currentTime
if (focusedView == viewName) {
if (StateManager.focusedView == viewName) {
setActiveView("HIDDEN")
} else {
setActiveView(viewName)
}
}
// ImageCanvas with forced size
val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
background = WIDGET_COLOR
preferredSize = imageSize
maximumSize = imageSize
minimumSize = imageSize
setFixedSize(imageSize)
}
// Wrapping the ImageCanvas in another JPanel to prevent stretching
val imageCanvasWrapper = JPanel().apply {
layout = GridBagLayout() // Keeps the layout of the wrapped panel minimal
preferredSize = imageSize
maximumSize = imageSize
minimumSize = imageSize
setFixedSize(imageSize)
isOpaque = false // No background for the wrapper
add(imageCanvas) // Adding ImageCanvas directly, layout won't stretch it
}
val panelButton = JPanel().apply {
layout = GridBagLayout()
preferredSize = buttonSize
maximumSize = buttonSize
minimumSize = buttonSize
setFixedSize(buttonSize)
background = WIDGET_COLOR
isOpaque = true
@ -575,7 +642,6 @@ class plugin : Plugin() {
add(imageCanvasWrapper, gbc)
// Hover and click behavior
val hoverListener = object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent?) {
background = WIDGET_COLOR.darker()
@ -666,7 +732,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)

View file

@ -1,3 +1,3 @@
AUTHOR='downthecrop'
DESCRIPTION='A plugin that adds a right-side panel with custom widgets and navigation.'
VERSION=2.0
VERSION=2.1

View file

@ -0,0 +1,26 @@
package KondoKit.pluginmanager
object GitLabConfig {
const val GITLAB_PROJECT_ID = "38297322"
const val GITLAB_PROJECT_PATH = "2009scape/tools/client-plugins"
const val GITLAB_BRANCH = "master"
const val DEBUG = false // Spams pretty hard
fun getGitLabApiBaseUrl(): String = "https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID"
fun getRepositoryTreeUrl(): String =
"${getGitLabApiBaseUrl()}/repository/tree?ref=${GITLAB_BRANCH}&per_page=100"
fun getRawFileUrl(filePath: String): String =
"${getGitLabApiBaseUrl()}/repository/files/${filePath.replace("/", "%2F")}/raw?ref=${GITLAB_BRANCH}"
fun getArchiveBaseUrl(): String =
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip"
fun getArchiveUrl(pluginPath: String): String =
"${getArchiveBaseUrl()}?path=$pluginPath"
fun getArchiveUrlWithRefType(pluginPath: String): String =
"${getArchiveBaseUrl()}?path=$pluginPath&ref_type=heads"
}

View file

@ -0,0 +1,134 @@
package KondoKit.pluginmanager
import KondoKit.pluginmanager.GitLabPlugin
import KondoKit.pluginmanager.PluginProperties
import KondoKit.util.HttpFetcher
import KondoKit.pluginmanager.GitLabConfig
import KondoKit.util.HttpStatusException
import KondoKit.util.JsonParser
import com.google.gson.JsonObject
import java.util.concurrent.*
import javax.swing.SwingUtilities
object GitLabPluginFetcher {
private const val TAG = "GitLabPluginFetcher"
private val executorService = Executors.newFixedThreadPool(5)
private fun debugLog(message: String) = PluginLogger.debug(TAG, message)
fun fetchGitLabPlugins(onComplete: (List<GitLabPlugin>) -> Unit) {
Thread {
val plugins = mutableListOf<GitLabPlugin>()
try {
debugLog("Starting to fetch GitLab plugins...")
val apiUrl = GitLabConfig.getRepositoryTreeUrl()
debugLog("API URL: $apiUrl")
val response = HttpFetcher.fetchString(apiUrl)
debugLog("Response length: ${response.length} characters")
debugLog("Response preview: ${response.take(200)}...")
val treeItems = JsonParser.fromJson(response, Array<JsonObject>::class.java)
debugLog("Parsed ${treeItems.size} items from JSON")
val pluginDirectories = mutableListOf<Pair<String, String>>()
// Filter for directories (trees) by mode 040000 so we only consider plugin folders
for (jsonObject in treeItems) {
if (jsonObject["type"].asString == "tree" && jsonObject["mode"].asString == "040000") {
val folderId = jsonObject["id"].asString
val folderPath = jsonObject["path"].asString
pluginDirectories.add(folderId to folderPath)
debugLog("Found directory: $folderPath (ID: $folderId)")
}
}
debugLog("Found ${pluginDirectories.size} plugin directories")
val pluginFutures = mutableListOf<Future<GitLabPlugin>>()
for ((folderId, folderPath) in pluginDirectories) {
val future = executorService.submit(Callable<GitLabPlugin> {
try {
val pluginProperties = fetchPluginProperties(folderPath)
debugLog("Successfully fetched properties for: $folderPath")
GitLabPlugin(folderId, folderPath, pluginProperties)
} catch (e: Exception) {
debugLog("Error fetching plugin.properties for $folderPath: ${e.message}")
GitLabPlugin(folderId, folderPath, null)
}
})
pluginFutures.add(future)
}
for (future in pluginFutures) {
try {
plugins.add(future.get())
} catch (e: Exception) {
debugLog("Error getting plugin result: ${e.message}")
}
}
} catch (e: HttpStatusException) {
debugLog("HTTP error ${e.statusCode} fetching plugin tree: ${e.body.take(200)}")
} catch (e: Exception) {
debugLog("Exception occurred: ${e.message}")
e.printStackTrace()
}
debugLog("Completed fetching plugins. Total plugins: ${plugins.size}")
SwingUtilities.invokeLater {
onComplete(plugins)
}
}.start()
}
private fun fetchPluginProperties(folderPath: String): PluginProperties {
val pluginFilePath = "$folderPath/plugin.properties"
val pluginUrl = GitLabConfig.getRawFileUrl(pluginFilePath)
debugLog("Fetching plugin.properties from: $pluginUrl")
try {
val rawContent = HttpFetcher.fetchString(pluginUrl)
debugLog("plugin.properties content length: ${rawContent.length} characters")
debugLog("plugin.properties content preview: ${rawContent.take(200)}...")
return parseProperties(rawContent)
} catch (e: HttpStatusException) {
debugLog("Failed to fetch plugin.properties. Response code: ${e.statusCode}")
debugLog("Error response for plugin.properties: ${e.body.take(200)}")
throw Exception("Plugin file not found for folder: $folderPath", e)
}
}
private fun parseProperties(content: String): PluginProperties {
debugLog("Parsing plugin.properties content")
val lines = content.split("\n")
var author = "Unknown"
var version = "Unknown"
var description = "No description available"
for (line in lines) {
val parts = line.split("=")
if (parts.size == 2) {
val key = parts[0].trim()
val value = parts[1].replace("\"", "").replace("'", "").trim()
when {
key.startsWith("AUTHOR", ignoreCase = true) -> {
author = value
debugLog("Found author: $value")
}
key.startsWith("VERSION", ignoreCase = true) -> {
version = value
debugLog("Found version: $value")
}
key.startsWith("DESCRIPTION", ignoreCase = true) -> {
description = value
debugLog("Found description: $value")
}
}
}
}
debugLog("Parsed properties - Author: $author, Version: $version, Description: $description")
return PluginProperties(author, version, description)
}
}

View file

@ -0,0 +1,339 @@
package KondoKit.pluginmanager
import KondoKit.util.HttpFetcher
import KondoKit.pluginmanager.GitLabConfig
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import javax.swing.SwingUtilities
/**
* Manages downloading and installing plugins from GitLab with concurrent support
*/
object PluginDownloadManager {
private const val TAG = "PluginDownloadManager"
private const val MAX_CONCURRENT_DOWNLOADS = 3
private val downloadExecutor = Executors.newFixedThreadPool(MAX_CONCURRENT_DOWNLOADS)
interface DownloadProgressCallback {
fun onProgress(pluginName: String, progress: Int)
fun onComplete(pluginName: String, success: Boolean, errorMessage: String? = null)
}
private fun debugLog(message: String) = PluginLogger.debug(TAG, message)
fun getDownloadUrlForLogging(plugin: GitLabPlugin): String {
return GitLabConfig.getArchiveUrl(plugin.path)
}
/**
* Download a single plugin with progress updates
*/
fun downloadPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback) {
downloadExecutor.submit {
try {
debugLog("Starting download for plugin: ${plugin.path}")
callback.onProgress(plugin.path, 0)
val success = downloadAndExtractPlugin(plugin, callback)
if (success) {
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
SwingUtilities.invokeLater {
callback.onComplete(plugin.path, true)
}
} else {
debugLog("Failed to download plugin: ${plugin.path}")
SwingUtilities.invokeLater {
callback.onComplete(plugin.path, false, "Failed to download plugin")
}
}
} catch (e: Exception) {
debugLog("Exception during download of ${plugin.path}: ${e.message}")
e.printStackTrace()
SwingUtilities.invokeLater {
callback.onComplete(plugin.path, false, e.message)
}
}
}
}
/**
* Download multiple plugins concurrently
*/
fun downloadPlugins(plugins: List<GitLabPlugin>, callback: (String, Boolean, String?) -> Unit) {
debugLog("Starting concurrent download of ${plugins.size} plugins")
for (plugin in plugins) {
downloadPlugin(plugin, object : DownloadProgressCallback {
override fun onProgress(pluginName: String, progress: Int) {
}
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
callback(pluginName, success, errorMessage)
}
})
}
}
/**
* Download and extract a plugin from GitLab
*/
private fun downloadAndExtractPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback): Boolean {
try {
if (plugin.path.isBlank()) {
debugLog("Plugin path is blank for plugin: ${plugin.path}")
return false
}
debugLog("Starting download for plugin: ${plugin.path}")
// Try multiple URL formats since GitLab has changed their API
val downloadUrls = listOf(
// Format 1: Using ref_type parameter
GitLabConfig.getArchiveUrlWithRefType(plugin.path),
// Format 2: Using ref parameter
GitLabConfig.getArchiveUrl(plugin.path),
GitLabConfig.getArchiveBaseUrl()
)
for (downloadUrl in downloadUrls) {
debugLog("Trying download URL: $downloadUrl")
val url = try {
URL(downloadUrl)
} catch (e: Exception) {
debugLog("Invalid URL: $downloadUrl for plugin: ${plugin.path}. Error: ${e.message}")
continue
}
val connection = try {
HttpFetcher.openGetConnection(url.toString())
} catch (e: Exception) {
debugLog("Failed to open connection for plugin: ${plugin.path} with URL: $downloadUrl. Error: ${e.message}")
continue
}
debugLog("Connection opened for plugin: ${plugin.path}")
debugLog("Request headers - User-Agent: ${connection.getRequestProperty("User-Agent")}")
debugLog("Request headers - Accept: ${connection.getRequestProperty("Accept")}")
val contentLength = connection.contentLength
debugLog("Content length: $contentLength bytes for plugin: ${plugin.path}")
val responseCode = connection.responseCode
debugLog("Response code: $responseCode for plugin: ${plugin.path}")
val responseMessage = connection.responseMessage
debugLog("Response message: $responseMessage for plugin: ${plugin.path}")
if (responseCode == HttpURLConnection.HTTP_OK) {
val inputStream = connection.inputStream
if (inputStream == null) {
debugLog("Input stream is null for plugin: ${plugin.path}")
continue
}
debugLog("Input stream available for plugin: ${plugin.path}")
val pluginsDir = File(rt4.GlobalJsonConfig.instance.pluginsFolder)
debugLog("Plugins directory: ${pluginsDir.absolutePath}")
if (!pluginsDir.exists()) {
debugLog("Plugins directory does not exist: ${pluginsDir.absolutePath}")
if (!pluginsDir.mkdirs()) {
debugLog("Failed to create plugins directory: ${pluginsDir.absolutePath}")
return false
}
debugLog("Created plugins directory: ${pluginsDir.absolutePath}")
}
if (!pluginsDir.isDirectory) {
debugLog("Plugins path is not a directory: ${pluginsDir.absolutePath}")
return false
}
val pluginDir = File(pluginsDir, plugin.path)
debugLog("Plugin directory: ${pluginDir.absolutePath}")
if (!pluginDir.exists()) {
pluginDir.mkdirs()
debugLog("Created plugin directory: ${pluginDir.absolutePath}")
}
val tempZipFile = File.createTempFile("plugin_", ".zip")
tempZipFile.deleteOnExit()
debugLog("Created temp file: ${tempZipFile.absolutePath}")
var totalBytesRead = 0L
inputStream.use { input ->
FileOutputStream(tempZipFile).use { outputStream ->
val buffer = ByteArray(8192)
var bytesRead: Int
debugLog("Starting to read data for plugin: ${plugin.path}")
while (input.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalBytesRead += bytesRead
// Report progress if content length is known
if (contentLength > 0) {
val progress = (totalBytesRead * 100 / contentLength).toInt()
// Limit progress updates to avoid flooding the UI
if (progress % 5 == 0) {
SwingUtilities.invokeLater {
callback.onProgress(plugin.path, progress)
}
}
}
}
debugLog("Finished reading data for plugin: ${plugin.path}. Total bytes: $totalBytesRead")
}
}
debugLog("Downloaded ${totalBytesRead} bytes to ${tempZipFile.absolutePath}")
if (extractZipFile(tempZipFile, pluginDir, plugin.path)) {
debugLog("Successfully extracted plugin to ${pluginDir.absolutePath}")
tempZipFile.delete()
return true
} else {
debugLog("Failed to extract plugin")
tempZipFile.delete()
continue // Try next URL format
}
} else {
debugLog("HTTP error: $responseCode for plugin: ${plugin.path} with URL: $downloadUrl")
// Try to read error stream for more details
try {
val errorStream = connection.errorStream
if (errorStream != null) {
val errorReader = BufferedReader(InputStreamReader(errorStream))
val errorResponse = errorReader.use { it.readText() }
debugLog("Error response: $errorResponse for plugin: ${plugin.path}")
} else {
debugLog("Error stream is null for plugin: ${plugin.path}")
}
} catch (e: Exception) {
debugLog("Exception while reading error stream: ${e.message} for plugin: ${plugin.path}")
}
continue // Try next URL format
}
}
debugLog("All URL formats failed for plugin: ${plugin.path}")
return false
} catch (e: Exception) {
debugLog("Exception during download and extraction for plugin ${plugin.path}: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* Extract a ZIP file to the specified directory
*/
private fun extractZipFile(zipFile: File, targetDir: File, pluginPath: String): Boolean {
try {
debugLog("Extracting ZIP file: ${zipFile.absolutePath} to ${targetDir.absolutePath}")
if (!zipFile.exists()) {
debugLog("ZIP file does not exist: ${zipFile.absolutePath}")
return false
}
if (!targetDir.exists() && !targetDir.mkdirs()) {
debugLog("Failed to create target directory: ${targetDir.absolutePath}")
return false
}
ZipInputStream(FileInputStream(zipFile)).use { zis ->
var entry: ZipEntry?
var entryCount = 0
while (zis.nextEntry.also { entry = it } != null) {
entryCount++
val entryName = entry!!.name
debugLog("Processing ZIP entry $entryCount: $entryName")
// Strip the top-level GitLab archive directory so we extract only the plugin contents
val relativePath = if (entryName.contains("/")) {
entryName.substring(entryName.indexOf("/") + 1)
} else {
entryName
}
debugLog("Relative path for entry: $relativePath")
if (relativePath.startsWith(pluginPath) && relativePath != pluginPath) {
val fileName = relativePath.substring(pluginPath.length + 1) // +1 for the trailing slash
debugLog("File name to extract: $fileName")
if (fileName.isNotEmpty()) {
val file = File(targetDir, fileName)
debugLog("Target file path: ${file.absolutePath}")
val parent = file.parentFile
if (parent != null && !parent.exists()) {
if (parent.mkdirs()) {
debugLog("Created parent directories: ${parent.absolutePath}")
} else {
debugLog("Failed to create parent directories: ${parent.absolutePath}")
zis.closeEntry()
continue
}
}
if (entry!!.isDirectory) {
if (!file.exists()) {
if (file.mkdirs()) {
debugLog("Created directory: ${file.absolutePath}")
} else {
debugLog("Failed to create directory: ${file.absolutePath}")
}
} else {
debugLog("Directory already exists: ${file.absolutePath}")
}
} else {
try {
FileOutputStream(file).use { fos ->
zis.copyTo(fos)
}
debugLog("Extracted file: ${file.absolutePath}")
} catch (e: Exception) {
debugLog("Failed to extract file ${file.absolutePath}: ${e.message}")
}
}
} else {
debugLog("Skipping empty file name")
}
} else {
debugLog("Skipping entry (not part of plugin): $relativePath")
}
zis.closeEntry()
}
debugLog("Processed $entryCount entries from ZIP file")
}
debugLog("Successfully extracted ZIP file")
return true
} catch (e: Exception) {
debugLog("Exception during ZIP extraction: ${e.message}")
e.printStackTrace()
return false
}
}
}

View file

@ -0,0 +1,9 @@
package KondoKit.pluginmanager
object PluginLogger {
fun debug(tag: String, message: String) {
if (GitLabConfig.DEBUG) {
println("[$tag] $message")
}
}
}

View file

@ -0,0 +1,29 @@
package KondoKit.pluginmanager
data class GitLabPlugin(
val id: String,
val path: String,
val pluginProperties: PluginProperties?
)
data class PluginProperties(
val author: String,
val version: String,
val description: String
)
/**
* Data class representing a plugin with its installation status and update information
*/
data class PluginInfoWithStatus(
val name: String,
val installedVersion: String?,
val remoteVersion: String?,
val description: String?,
val author: String?,
val gitLabPlugin: GitLabPlugin?,
val isInstalled: Boolean,
val needsUpdate: Boolean,
val isDownloading: Boolean = false,
val downloadProgress: Int = 0
)

View file

@ -0,0 +1,405 @@
package KondoKit.pluginmanager
import KondoKit.util.FileUtils
import KondoKit.util.Helpers
import KondoKit.views.ReflectiveEditorView
import plugin.Plugin
import plugin.PluginInfo
import plugin.PluginRepository
import rt4.GlobalJsonConfig
import java.awt.Component
import java.io.File
import java.util.*
import javax.swing.*
class ReflectiveEditorPlugin : Plugin() {
private var gitLabPlugins: List<GitLabPlugin> = listOf()
private var pluginStatuses: List<PluginInfoWithStatus> = listOf()
private var reloadPlugins = false // Flag for scheduled plugin reload to avoid crashes
private val pluginsDirectory: File = File(GlobalJsonConfig.instance.pluginsFolder)
override fun Init() {
GitLabPluginFetcher.fetchGitLabPlugins { plugins ->
gitLabPlugins = plugins
updatePluginStatuses()
}
}
fun getGitLabPlugins(): List<GitLabPlugin> = gitLabPlugins
fun getPluginStatuses(): List<PluginInfoWithStatus> = pluginStatuses
fun updatePluginStatuses() {
val loadedPluginNames = getLoadedPluginNames()
val statuses = mutableListOf<PluginInfoWithStatus>()
val disabledPluginInfo = getDisabledPluginInfo()
handleDuplicatePlugins(loadedPluginNames, disabledPluginInfo)
for (gitLabPlugin in gitLabPlugins) {
val pluginName = gitLabPlugin.path
// Skip KondoKit since it should always be loaded
if (isKondoKit(pluginName)) {
continue
}
val remoteVersion = gitLabPlugin.pluginProperties?.version ?: "Unknown"
val description = gitLabPlugin.pluginProperties?.description ?: "No description available"
val author = gitLabPlugin.pluginProperties?.author ?: "Unknown"
val isLoaded = loadedPluginNames.contains(pluginName)
val isDisabled = disabledPluginInfo.containsKey(pluginName)
val disabledVersion = disabledPluginInfo[pluginName]
val existingStatus = pluginStatuses.find { it.name == pluginName }
val isDownloading = existingStatus?.isDownloading ?: false
val downloadProgress = existingStatus?.downloadProgress ?: 0
if (isLoaded) {
statuses.add(PluginInfoWithStatus(
name = pluginName,
installedVersion = remoteVersion, // We don't have the actual installed version, but we know it's loaded
remoteVersion = remoteVersion,
description = description,
author = author,
gitLabPlugin = gitLabPlugin,
isInstalled = true,
needsUpdate = false, // We assume it's up-to-date since it's loaded
isDownloading = isDownloading,
downloadProgress = downloadProgress
))
} else if (isDisabled) {
val versionsMatch = disabledVersion != null && disabledVersion == remoteVersion
if (!versionsMatch) {
statuses.add(PluginInfoWithStatus(
name = pluginName,
installedVersion = disabledVersion,
remoteVersion = remoteVersion,
description = description,
author = author,
gitLabPlugin = gitLabPlugin,
isInstalled = true, // It's installed but disabled
needsUpdate = true, // Needs update since versions don't match
isDownloading = isDownloading,
downloadProgress = downloadProgress
))
}
} else {
statuses.add(PluginInfoWithStatus(
name = pluginName,
installedVersion = null,
remoteVersion = remoteVersion,
description = description,
author = author,
gitLabPlugin = gitLabPlugin,
isInstalled = false,
needsUpdate = false,
isDownloading = isDownloading,
downloadProgress = downloadProgress
))
}
}
pluginStatuses = statuses
}
private fun getLoadedPluginNames(): Set<String> {
val loadedPluginNames = mutableSetOf<String>()
try {
val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins")
loadedPluginsField.isAccessible = true
val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *>
for ((_, plugin) in loadedPlugins) {
val pluginName = getPluginDirName(plugin as Plugin)
loadedPluginNames.add(pluginName)
}
} catch (e: Exception) {
e.printStackTrace()
}
return loadedPluginNames
}
private fun parsePluginProperties(content: String): PluginProperties {
var author = "Unknown"
var version = "Unknown"
var description = "No description available"
val lines = content.split("\n")
for (line in lines) {
val parts = line.split("=")
if (parts.size == 2) {
val key = parts[0].trim()
val value = parts[1].replace("\"", "").replace("'", "").trim()
when {
key.startsWith("AUTHOR", ignoreCase = true) -> {
author = value
}
key.startsWith("VERSION", ignoreCase = true) -> {
version = value
}
key.startsWith("DESCRIPTION", ignoreCase = true) -> {
description = value
}
}
}
}
return PluginProperties(author, version, description)
}
private fun getDisabledPluginInfo(): Map<String, String> {
val disabledPluginInfo = mutableMapOf<String, String>()
val disabledDir = File(pluginsDirectory, "disabled")
if (disabledDir.exists() && disabledDir.isDirectory) {
val disabledPlugins = disabledDir.listFiles { file -> file.isDirectory } ?: arrayOf()
for (pluginDir in disabledPlugins) {
try {
val propertiesFile = File(pluginDir, "plugin.properties")
if (propertiesFile.exists()) {
val content = propertiesFile.readText()
val properties = parsePluginProperties(content)
disabledPluginInfo[pluginDir.name] = properties.version
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
return disabledPluginInfo
}
private fun handleDuplicatePlugins(loadedPluginNames: Set<String>, disabledPluginInfo: Map<String, String>) {
var needsReload = false
for (pluginName in loadedPluginNames) {
if (disabledPluginInfo.containsKey(pluginName)) {
try {
val disabledDir = File(pluginsDirectory, "disabled")
val pluginDir = File(disabledDir, pluginName)
if (pluginDir.exists() && pluginDir.isDirectory) {
if (FileUtils.deleteRecursively(pluginDir)) {
needsReload = true
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// Reload plugins if we deleted any disabled duplicates
if (needsReload) {
PluginRepository.reloadPlugins()
}
}
fun deletePlugin(pluginName: String, isDisabled: Boolean, mainPanel: Component) {
try {
val pluginDir = if (isDisabled) {
File(File(pluginsDirectory, "disabled"), pluginName)
} else {
File(pluginsDirectory, pluginName)
}
if (pluginDir.exists() && pluginDir.isDirectory) {
if (FileUtils.deleteRecursively(pluginDir)) {
Helpers.showToast(mainPanel, "Plugin deleted successfully", JOptionPane.INFORMATION_MESSAGE)
if (!isDisabled) {
reloadPlugins = true
}
SwingUtilities.invokeLater {
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
} else {
Helpers.showToast(mainPanel, "Failed to delete plugin", JOptionPane.ERROR_MESSAGE)
}
} else {
Helpers.showToast(mainPanel, "Plugin directory not found", JOptionPane.ERROR_MESSAGE)
}
} catch (e: Exception) {
e.printStackTrace()
Helpers.showToast(mainPanel, "Error deleting plugin: ${e.message}", JOptionPane.ERROR_MESSAGE)
}
}
private fun isKondoKit(pluginName: String): Boolean {
return pluginName == "KondoKit"
}
fun enablePlugin(pluginName: String, mainPanel: Component) {
try {
val disabledDir = File(pluginsDirectory, "disabled")
val sourceDir = File(disabledDir, pluginName)
val destDir = File(pluginsDirectory, pluginName)
if (!sourceDir.exists()) {
Helpers.showToast(mainPanel, "Plugin directory not found: ${sourceDir.absolutePath}", JOptionPane.ERROR_MESSAGE)
return
}
if (sourceDir.renameTo(destDir)) {
Helpers.showToast(mainPanel, "Plugin enabled")
// Schedule plugin reload to avoid crashes
reloadPlugins = true
SwingUtilities.invokeLater {
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
} else {
Helpers.showToast(mainPanel, "Failed to enable plugin", JOptionPane.ERROR_MESSAGE)
}
} catch (e: Exception) {
e.printStackTrace()
Helpers.showToast(mainPanel, "Error enabling plugin: ${e.message}", JOptionPane.ERROR_MESSAGE)
}
}
fun togglePlugin(plugin: Plugin, pluginInfo: PluginInfo, toggleSwitch: Component, activated: Boolean, mainPanel: Component) {
try {
val pluginDirName = getPluginDirName(plugin)
val sourceDir = if (activated) {
File(File(pluginsDirectory, "disabled"), pluginDirName)
} else {
File(pluginsDirectory, pluginDirName)
}
val destDir = if (activated) {
File(pluginsDirectory, pluginDirName)
} else {
val disabledDir = File(pluginsDirectory, "disabled")
if (!disabledDir.exists()) {
disabledDir.mkdirs()
}
File(disabledDir, pluginDirName)
}
if (!sourceDir.exists()) {
Helpers.showToast(mainPanel, "Plugin directory not found: ${sourceDir.absolutePath}", JOptionPane.ERROR_MESSAGE)
SwingUtilities.invokeLater {
if (toggleSwitch is JComponent) {
toggleSwitch.repaint()
}
}
return
}
if (sourceDir.renameTo(destDir)) {
Helpers.showToast(mainPanel, if (activated) "Plugin enabled" else "Plugin disabled")
// Schedule plugin reload to avoid crashes
reloadPlugins = true
SwingUtilities.invokeLater {
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
} else {
Helpers.showToast(mainPanel, "Failed to ${if (activated) "enable" else "disable"} plugin", JOptionPane.ERROR_MESSAGE)
SwingUtilities.invokeLater {
if (toggleSwitch is JComponent) {
toggleSwitch.repaint()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
Helpers.showToast(mainPanel, "Error toggling plugin: ${e.message}", JOptionPane.ERROR_MESSAGE)
SwingUtilities.invokeLater {
if (toggleSwitch is JComponent) {
toggleSwitch.repaint()
}
}
}
}
private fun getPluginDirName(plugin: Plugin): String {
// Extract the directory name from the plugin's package
// The package name is typically like "GroundItems.plugin" so we take the first part
val packageName = plugin.javaClass.`package`.name
return packageName.substringBefore(".")
}
fun isPluginEnabled(plugin: Plugin, pluginInfo: PluginInfo): Boolean {
val pluginDirName = getPluginDirName(plugin)
val pluginDir = File(pluginsDirectory, pluginDirName)
return pluginDir.exists() && pluginDir.isDirectory
}
fun startPluginDownload(pluginStatus: PluginInfoWithStatus, mainPanel: Component) {
val gitLabPlugin = pluginStatus.gitLabPlugin
if (gitLabPlugin == null) {
Helpers.showToast(mainPanel, "Plugin information not available", JOptionPane.ERROR_MESSAGE)
return
}
val updatedStatuses = pluginStatuses.map {
if (it.name == pluginStatus.name) {
it.copy(isDownloading = true, downloadProgress = 0)
} else {
it
}
}
pluginStatuses = updatedStatuses
SwingUtilities.invokeLater {
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
PluginDownloadManager.downloadPlugin(gitLabPlugin, object : PluginDownloadManager.DownloadProgressCallback {
override fun onProgress(pluginName: String, progress: Int) {
val updatedStatuses = pluginStatuses.map {
if (it.name == pluginName) {
it.copy(downloadProgress = progress)
} else {
it
}
}
pluginStatuses = updatedStatuses
SwingUtilities.invokeLater {
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
}
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
val updatedStatuses = pluginStatuses.map {
if (it.name == pluginName) {
it.copy(isDownloading = false, downloadProgress = if (success) 100 else 0)
} else {
it
}
}
pluginStatuses = updatedStatuses
if (success) {
Helpers.showToast(mainPanel, "Plugin downloaded successfully!", JOptionPane.INFORMATION_MESSAGE)
// Reload plugins to make the newly downloaded plugin available
PluginRepository.reloadPlugins()
} else {
Helpers.showToast(mainPanel, "Failed to download plugin: ${errorMessage ?: "Unknown error"}", JOptionPane.ERROR_MESSAGE)
}
SwingUtilities.invokeLater {
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
}
}
})
}
fun getPluginsDirectory(): File = pluginsDirectory
fun shouldReloadPlugins(): Boolean = reloadPlugins
fun setReloadPlugins(reload: Boolean) {
reloadPlugins = reload
}
}

View file

@ -0,0 +1,31 @@
package KondoKit.ui
import KondoKit.ui.ViewConstants
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.util.setFixedSize
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JPanel
open class BaseView(
private val viewName: String,
private val preferredWidth: Int = ViewConstants.DEFAULT_PANEL_SIZE.width,
private val addDefaultSpacing: Boolean = true
) : JPanel() {
init {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
name = viewName
background = VIEW_BACKGROUND_COLOR
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
if (addDefaultSpacing) {
add(Box.createVerticalStrut(5))
}
}
fun setViewSize(height: Int) {
setFixedSize(preferredWidth, height)
}
}

View file

@ -0,0 +1,12 @@
package KondoKit.ui
import javax.swing.JPanel
interface View {
val name: String
val iconSpriteId: Int
val panel: JPanel
fun createView()
fun registerFunctions()
}

View file

@ -0,0 +1,21 @@
package KondoKit.ui
interface OnUpdateCallback {
fun onUpdate()
}
interface OnDrawCallback {
fun onDraw(timeDelta: Long)
}
interface OnXPUpdateCallback {
fun onXPUpdate(skillId: Int, xp: Int)
}
interface OnKillingBlowNPCCallback {
fun onKillingBlowNPC(npcID: Int, x: Int, z: Int)
}
interface OnPostClientTickCallback {
fun onPostClientTick()
}

View file

@ -0,0 +1,41 @@
package KondoKit.ui
import java.awt.Dimension
import java.awt.Font
object ViewConstants {
val FONT_RUNESCAPE_SMALL_16 = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val FONT_RUNESCAPE_SMALL_14 = Font("RuneScape Small", Font.PLAIN, 14)
val FONT_RUNESCAPE_SMALL_PLAIN_16 = Font("RuneScape Small", Font.PLAIN, 16)
val FONT_RUNESCAPE_SMALL_BOLD_16 = Font("RuneScape Small", Font.BOLD, 16)
val FONT_ARIAL_PLAIN_14 = Font("Arial", Font.PLAIN, 14)
val FONT_ARIAL_BOLD_12 = Font("Arial", Font.BOLD, 12)
val DIMENSION_SMALL_ICON = Dimension(12, 12)
val DIMENSION_LARGE_ICON = Dimension(30, 30)
val DEFAULT_WIDGET_SIZE = Dimension(234, 50)
val PLUGIN_LIST_ITEM_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width, 36)
val TOGGLE_PLACEHOLDER_SIZE = Dimension(60, 24)
val TOTAL_XP_WIDGET_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width, 42)
val IMAGE_SIZE = Dimension(25, 23)
val SEARCH_FIELD_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width, 30)
val DEFAULT_PANEL_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width, 500)
val FILTER_PANEL_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width, 30)
val SKILLS_PANEL_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width, 290)
val TOTAL_COMBAT_PANEL_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width, 30)
val SKILL_PANEL_SIZE = Dimension(DEFAULT_WIDGET_SIZE.width / 3, 35)
val IMAGE_CANVAS_SIZE = Dimension(20, 20)
val SKILL_SPRITE_SIZE = Dimension(14, 14)
val NUMBER_LABEL_SIZE = Dimension(20, 20)
val PROGRESS_BAR_SIZE = Dimension(220, 16)
val TOGGLE_SWITCH_SIZE = Dimension(32, 20)
val SKILL_DISPLAY_ORDER = arrayOf(0, 3, 14, 2, 16, 13, 1, 15, 10, 4, 17, 7, 5, 12, 11, 6, 9, 8, 20, 18, 19, 22, 21, 23)
const val COMBAT_LVL_SPRITE = 168
const val IRONMAN_SPRITE = 4
const val MAG_SPRITE = 1423
const val LVL_BAR_SPRITE = 898
const val WRENCH_ICON = 907
const val LOOT_ICON = 777
}

View file

@ -0,0 +1,44 @@
package KondoKit.ui.components
import KondoKit.ui.ViewConstants
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.ui.components.UiStyler.button
import KondoKit.ui.components.UiStyler.ButtonDefaults
import java.awt.*
import javax.swing.*
class ButtonPanel(
private val alignment: Int = FlowLayout.CENTER,
private val hgap: Int = 5,
private val vgap: Int = 0
) : JPanel() {
init {
layout = FlowLayout(alignment, hgap, vgap)
background = WIDGET_COLOR
}
fun addButton(text: String, action: () -> Unit): JButton =
createAndAddButton(text = text, action = action)
fun addIcon(icon: Icon, action: () -> Unit): JButton =
createAndAddButton(icon = icon, action = action)
private fun createAndAddButton(
text: String? = null,
icon: Icon? = null,
action: () -> Unit
): JButton {
val defaults = ButtonDefaults(
background = TITLE_BAR_COLOR,
foreground = secondaryColor,
font = ViewConstants.FONT_RUNESCAPE_SMALL_14
)
return button(text = text, icon = icon, defaults = defaults, onClick = action).also {
add(it)
}
}
}

View file

@ -0,0 +1,55 @@
package KondoKit.ui.components
import KondoKit.util.ImageCanvas
import KondoKit.util.SpriteToBufferedImage
import KondoKit.util.setFixedSize
import KondoKit.plugin.Companion.WIDGET_COLOR
import plugin.api.API
import java.awt.BorderLayout
import java.awt.Color
import javax.swing.BorderFactory
import javax.swing.JPanel
class IconComponent(
spriteId: Int,
private val iconWidth: Int = 25,
private val iconHeight: Int = 23,
private val useThemeColors: Boolean = true,
private val tint: Color? = null,
private val grayscale: Boolean = false,
private val brightnessBoost: Float = 1.0f
) : JPanel() {
private val imageCanvas: ImageCanvas
init {
layout = BorderLayout()
background = if (useThemeColors) WIDGET_COLOR else Color.WHITE
val bufferedImageSprite = SpriteToBufferedImage.getBufferedImageFromSprite(
API.GetSprite(spriteId),
tint,
grayscale,
brightnessBoost
)
imageCanvas = ImageCanvas(bufferedImageSprite).apply {
setFixedSize(iconWidth, iconHeight)
background = if (useThemeColors) WIDGET_COLOR else Color.WHITE
}
add(imageCanvas, BorderLayout.CENTER)
border = BorderFactory.createEmptyBorder(2, 2, 2, 2)
}
fun updateFillColor(color: Color) {
imageCanvas.fillColor = color
background = color
imageCanvas.repaint()
repaint()
}
fun applyThemeColors() {
updateFillColor(WIDGET_COLOR)
}
}

View file

@ -0,0 +1,36 @@
package KondoKit.ui.components
import KondoKit.util.Helpers.formatHtmlLabelText
import KondoKit.ui.ViewConstants
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import java.awt.Font
import java.awt.Color
import javax.swing.JLabel
import javax.swing.SwingConstants
class LabelComponent(
text: String = "",
private val isHtml: Boolean = false,
private val alignment: Int = SwingConstants.LEFT
) : JLabel() {
init {
this.text = text
this.horizontalAlignment = alignment
this.font = ViewConstants.FONT_RUNESCAPE_SMALL_16
}
fun updateText(plainText: String, color: Color = secondaryColor) {
this.text = plainText
this.foreground = color
}
fun updateHtmlText(text1: String, color1: Color = primaryColor, text2: String = "", color2: Color = secondaryColor) {
this.text = formatHtmlLabelText(text1, color1, text2, color2)
}
fun setAsHeader() {
font = ViewConstants.FONT_RUNESCAPE_SMALL_16.deriveFont(Font.BOLD)
}
}

View file

@ -0,0 +1,19 @@
package KondoKit.ui.components
import KondoKit.ui.components.UiStyler.menuItem
import KondoKit.plugin.Companion.POPUP_BACKGROUND
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
class PopupMenuComponent : JPopupMenu() {
init {
background = POPUP_BACKGROUND
}
fun addMenuItem(text: String, action: () -> Unit): JMenuItem {
val item = menuItem(text = text, onClick = action)
add(item)
return item
}
}

View file

@ -1,11 +1,11 @@
package KondoKit
package KondoKit.ui.components
import KondoKit.ui.ViewConstants
import KondoKit.plugin.Companion.PROGRESS_BAR_FILL
import KondoKit.plugin.Companion.secondaryColor
import java.awt.Canvas
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
import java.awt.Graphics
class ProgressBar(
@ -16,44 +16,38 @@ class ProgressBar(
) : Canvas() {
init {
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
font = ViewConstants.FONT_RUNESCAPE_SMALL_16
}
override fun paint(g: Graphics) {
super.paint(g)
// Draw the filled part of the progress bar
g.color = barColor
val width = (progress * this.width / 100).toInt()
g.fillRect(0, 0, width, this.height)
// Draw the unfilled part of the progress bar
g.color = PROGRESS_BAR_FILL
g.fillRect(width, 0, this.width - width, this.height)
// Variables for text position
val textY = this.height / 2 + 6
// Draw the current level on the far left
drawTextWithShadow(g, "Lvl. $currentLevel", 5, textY, secondaryColor)
// Draw the percentage in the middle
val percentageText = String.format("%.2f%%", progress)
val percentageWidth = g.fontMetrics.stringWidth(percentageText)
drawTextWithShadow(g, percentageText, (this.width - percentageWidth) / 2, textY, secondaryColor)
// Draw the next level on the far right
val nextLevelText = "Lvl. $nextLevel"
val nextLevelWidth = g.fontMetrics.stringWidth(nextLevelText)
drawTextWithShadow(g, nextLevelText, this.width - nextLevelWidth - 5, textY, secondaryColor)
}
override fun getPreferredSize(): Dimension {
return Dimension(220, 16) // Force the height to 16px, width can be anything appropriate
return ViewConstants.PROGRESS_BAR_SIZE
}
override fun getMinimumSize(): Dimension {
return Dimension(220, 16) // Force the minimum height to 16px, width can be smaller
return ViewConstants.PROGRESS_BAR_SIZE
}
fun updateProgress(newProgress: Double, currentLevel: Int, nextLevel: Int, isVisible : Boolean) {
@ -64,13 +58,10 @@ class ProgressBar(
repaint()
}
// Helper function to draw text with a shadow effect
private fun drawTextWithShadow(g: Graphics, text: String, x: Int, y: Int, textColor: Color) {
// Draw shadow (black text with -1 x and -1 y offset)
g.color = Color(0, 0, 0)
g.drawString(text, x + 1, y + 1)
// Draw actual text on top
g.color = textColor
g.drawString(text, x, y)
}

View file

@ -1,4 +1,4 @@
package KondoKit
package KondoKit.ui.components
import KondoKit.plugin.Companion.SCROLL_BAR_COLOR
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR

View file

@ -0,0 +1,190 @@
package KondoKit.ui.components
import KondoKit.util.ImageCanvas
import KondoKit.ui.ViewConstants
import KondoKit.util.SpriteToBufferedImage
import KondoKit.util.setFixedSize
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import plugin.api.API
import java.awt.*
import java.awt.datatransfer.DataFlavor
import java.awt.event.*
import javax.swing.*
class SearchField(
private val parentPanel: JPanel,
private val onSearch: (String) -> Unit,
private val placeholderText: String = "Search...",
private val fieldWidth: Int = ViewConstants.SEARCH_FIELD_SIZE.width,
private val fieldHeight: Int = ViewConstants.SEARCH_FIELD_SIZE.height,
private val viewName: String? = ""
) : Canvas() {
private var cursorVisible: Boolean = true
private var text: String = ""
private val bufferedImageSprite = SpriteToBufferedImage.getBufferedImageFromSprite(API.GetSprite(1423)) // MAG_SPRITE
private val imageCanvas = bufferedImageSprite.let {
ImageCanvas(it).apply {
setFixedSize(ViewConstants.DIMENSION_SMALL_ICON)
size = preferredSize
fillColor = WIDGET_COLOR
}
}
init {
val dimension = Dimension(fieldWidth, fieldHeight)
setFixedSize(dimension)
background = WIDGET_COLOR
foreground = secondaryColor
font = ViewConstants.FONT_ARIAL_PLAIN_14
isFocusable = true
focusTraversalKeysEnabled = false
addFocusListener(object : FocusAdapter() {
override fun focusGained(e: FocusEvent) {
cursorVisible = true
repaintAsync()
}
override fun focusLost(e: FocusEvent) {
cursorVisible = false
repaintAsync()
}
})
addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
// Prevent null character from being typed on Ctrl+A & Ctrl+V
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
e.consume()
return
}
if (e.keyChar == '\b') {
if (text.isNotEmpty()) {
text = text.dropLast(1)
}
} else if (e.keyChar == '\n') {
// Handled in keyPressed to avoid duplicate searches
} else {
text += e.keyChar
}
repaintAsync()
}
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_ENTER) {
triggerSearch()
e.consume()
return
}
if (e.isControlDown) {
when (e.keyCode) {
KeyEvent.VK_A -> {
// They probably want to clear the search box
text = ""
repaintAsync()
}
KeyEvent.VK_V -> {
try {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
text += pasteText
repaintAsync()
} catch (_: Exception) { }
}
}
}
}
})
addMouseListener(object : MouseAdapter() {
// Clicked the search field 'x' button
override fun mouseClicked(e: MouseEvent) {
requestFocusInWindow()
if (e.x > width - 20 && e.y < 20) {
text = ""
triggerSearch()
repaintAsync()
}
}
override fun mousePressed(e: MouseEvent) {
requestFocusInWindow()
}
})
Timer(500) { _ ->
cursorVisible = !cursorVisible
// Only repaint if the view is active or if viewName is not specified
if (viewName == null || focusedView == viewName) {
repaintAsync()
}
}.start()
}
override fun paint(g: Graphics) {
super.paint(g)
g.color = foreground
g.font = font
val fm = g.fontMetrics
val cursorX = fm.stringWidth(text) + 30
// Draw magnifying glass icon
imageCanvas.let { canvas ->
val imgG = g.create(5, 5, canvas.width, canvas.height)
canvas.paint(imgG)
imgG.dispose()
}
// Use a local copy of the text to avoid threading issues
val currentText = text
// Draw placeholder text if field is empty, otherwise draw actual text
if (currentText.isEmpty()) {
g.color = Color.GRAY // lighter color for placeholder
g.drawString(placeholderText, 30, 20)
} else {
g.color = foreground // normal color for user entered text
g.drawString(currentText, 30, 20)
}
if (cursorVisible && hasFocus()) {
g.color = foreground
g.drawLine(cursorX, 5, cursorX, 25)
}
// Only draw the "x" button if there's text
if (currentText.isNotEmpty()) {
g.color = Color.RED
g.drawString("x", width - 20, 20)
}
}
fun setText(newText: String) {
text = newText
repaintAsync()
}
fun getText(): String = text
private fun triggerSearch() {
val query = text.trim()
if (query.isNotEmpty()) {
text = query
repaintAsync()
onSearch(query)
} else {
onSearch(query) // Call with empty string for clearing filters
}
}
private fun repaintAsync() {
SwingUtilities.invokeLater { repaint() }
}
}

View file

@ -0,0 +1,488 @@
package KondoKit.ui.components
import KondoKit.util.Helpers
import KondoKit.util.Helpers.FieldNotifier
import KondoKit.ui.ViewConstants
import KondoKit.ui.components.UiStyler
import KondoKit.util.setFixedSize
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import plugin.Plugin
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.lang.reflect.Field
import java.util.*
import java.util.Timer
import javax.swing.*
import javax.swing.table.DefaultTableModel
class SettingsPanel(private val plugin: Plugin) : JPanel() {
init {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR
border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
}
fun addSettingsFromPlugin() {
val fieldNotifier = FieldNotifier(plugin)
val exposedFields = plugin.javaClass.declaredFields.filter { field ->
field.annotations.any { annotation ->
annotation.annotationClass.simpleName == "Exposed"
}
}
if (exposedFields.isNotEmpty()) {
for (field in exposedFields) {
field.isAccessible = true
// Get the "Exposed" annotation specifically and retrieve its description, if available
val description = field.exposedDescription()
val fieldPanel = JPanel().apply {
layout = GridBagLayout()
background = WIDGET_COLOR
foreground = secondaryColor
border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
maximumSize = Dimension(Int.MAX_VALUE, 50)
}
val gbc = GridBagConstraints().apply {
insets = Insets(0, 5, 0, 5)
}
val label = JLabel(field.name.capitalize()).apply {
foreground = secondaryColor
font = ViewConstants.FONT_RUNESCAPE_SMALL_16
}
gbc.gridx = 0
gbc.gridy = 0
gbc.weightx = 0.0
gbc.anchor = GridBagConstraints.WEST
fieldPanel.add(label, gbc)
// Create appropriate input component based on field type
val inputComponent: JComponent
val isHashMapEditor: Boolean
when {
field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> {
inputComponent = JCheckBox().apply {
isSelected = field.get(plugin) as Boolean
}
isHashMapEditor = false
}
field.type.isEnum -> {
val enumConstants = field.type.enumConstants
inputComponent = JComboBox(enumConstants as Array<out Enum<*>>).apply {
selectedItem = field.get(plugin)
}
isHashMapEditor = false
}
field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> {
inputComponent = JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1))
isHashMapEditor = false
}
field.type == Float::class.javaPrimitiveType ||
field.type == Double::class.javaPrimitiveType ||
field.type == java.lang.Float::class.java ||
field.type == java.lang.Double::class.java -> {
inputComponent = JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1))
isHashMapEditor = false
}
// Check if the field is a HashMap
field.isHashMapType() -> {
inputComponent = createHashMapEditor(field, plugin, fieldNotifier)
isHashMapEditor = true
}
else -> {
inputComponent = JTextField(field.get(plugin)?.toString() ?: "")
isHashMapEditor = false
}
}
// Handle HashMap editors differently - they don't use the fieldPanel layout
if (isHashMapEditor) {
// Add the HashMap editor directly to the settings panel
add(inputComponent)
} else {
// Add mouse listener to the label only if a description is available
if (description.isNotBlank()) {
label.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
label.addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
showCustomToolTip(description, label)
}
override fun mouseExited(e: MouseEvent) {
customToolTipWindow?.isVisible = false
}
})
}
gbc.gridx = 1
gbc.gridy = 0
gbc.weightx = 1.0
gbc.fill = GridBagConstraints.HORIZONTAL
fieldPanel.add(inputComponent, gbc)
// Add apply button for non-HashMap editors
val applyButton = UiStyler.button(
text = "=>",
onClick = {
try {
val newValue = when (inputComponent) {
is JCheckBox -> inputComponent.isSelected
is JComboBox<*> -> inputComponent.selectedItem
is JSpinner -> inputComponent.value
is JTextField -> Helpers.convertValue(field.type, field.genericType, inputComponent.text)
else -> throw IllegalArgumentException("Unsupported input component type")
}
fieldNotifier.setFieldValue(field, newValue)
Helpers.showToast(
this@SettingsPanel,
"${field.name} updated successfully!"
)
} catch (e: Exception) {
Helpers.showToast(
this@SettingsPanel,
"Failed to update ${field.name}: ${e.message}",
JOptionPane.ERROR_MESSAGE
)
}
}
).also {
it.maximumSize = Dimension(Int.MAX_VALUE, 8)
}
gbc.gridx = 2
gbc.gridy = 0
gbc.weightx = 0.0
gbc.fill = GridBagConstraints.NONE
fieldPanel.add(applyButton, gbc)
add(fieldPanel)
}
// Track field changes in real-time and update UI (skip HashMap fields)
if (!field.isHashMapType()) {
var previousValue = field.get(plugin)?.toString()
val timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
val currentValue = field.get(plugin)?.toString()
if (currentValue != previousValue) {
previousValue = currentValue
SwingUtilities.invokeLater {
// Update the inputComponent based on the new value
when (inputComponent) {
is JCheckBox -> inputComponent.isSelected = field.get(plugin) as Boolean
is JComboBox<*> -> inputComponent.selectedItem = field.get(plugin)
is JSpinner -> inputComponent.value = field.get(plugin)
is JTextField -> inputComponent.text = field.get(plugin)?.toString() ?: ""
}
}
}
}
}, 0, 1000) // Poll every 1000 milliseconds (1 second)
}
}
if (exposedFields.isNotEmpty()) {
add(Box.createVerticalStrut(5))
}
}
}
private fun createHashMapEditor(field: Field, plugin: Plugin, fieldNotifier: FieldNotifier): JComponent {
// Create a panel to hold the table and buttons
val editorPanel = JPanel()
editorPanel.layout = BoxLayout(editorPanel, BoxLayout.Y_AXIS)
editorPanel.background = WIDGET_COLOR
editorPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
editorPanel.maximumSize = Dimension(Int.MAX_VALUE, 250)
val description = field.exposedDescription()
// Create title label
val titleLabel = JLabel("${field.name} (Key-Value Pairs)").apply {
foreground = secondaryColor
font = ViewConstants.FONT_RUNESCAPE_SMALL_16
alignmentX = Component.LEFT_ALIGNMENT
// Add mouse listener to the label only if a description is available
if (description.isNotBlank()) {
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
showCustomToolTip(description, this@apply)
}
override fun mouseExited(e: MouseEvent) {
SettingsPanel.customToolTipWindow?.isVisible = false
}
})
}
}
// Get the current HashMap value
val hashMap = field.get(plugin) as? HashMap<*, *> ?: HashMap<Any, Any>()
// Create a table model for the HashMap
val tableModel = object : DefaultTableModel() {
init {
val columnVector = java.util.Vector<String>()
columnVector.add("Key")
columnVector.add("Value")
columnIdentifiers = columnVector
hashMap.forEach { (key, value) ->
val vector = java.util.Vector<Any>()
vector.add(key.toString())
vector.add(value.toString())
addRow(vector)
}
}
override fun isCellEditable(row: Int, column: Int): Boolean {
return true
}
override fun getColumnClass(columnIndex: Int): Class<*> {
return String::class.java
}
}
// Create the table
val table = JTable(tableModel).apply {
background = WIDGET_COLOR
foreground = secondaryColor
gridColor = primaryColor
tableHeader.background = WIDGET_COLOR
tableHeader.foreground = secondaryColor
preferredScrollableViewportSize = Dimension(Int.MAX_VALUE, 150)
setFillsViewportHeight(true)
}
// Create a scroll pane for the table with fixed size
val scrollPane = JScrollPane(table).apply {
setFixedSize(Dimension(Int.MAX_VALUE, 150))
background = WIDGET_COLOR
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
alignmentX = Component.LEFT_ALIGNMENT
}
// Create buttons panel
val buttonsPanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 5)).apply {
background = WIDGET_COLOR
alignmentX = Component.LEFT_ALIGNMENT
}
// Add button
val addButton = UiStyler.button(
text = "+",
onClick = {
val vector = java.util.Vector<Any>()
vector.add("")
vector.add("")
tableModel.addRow(vector)
// Select the new row for editing
val newRow = tableModel.rowCount - 1
table.setRowSelectionInterval(newRow, newRow)
table.editCellAt(newRow, 0)
table.editorComponent?.requestFocus()
}
).apply {
setFixedSize(40, 30)
}
// Remove button
val removeButton = UiStyler.button(
text = "-",
onClick = {
val selectedRow = table.selectedRow
if (selectedRow >= 0) {
tableModel.removeRow(selectedRow)
} else {
Helpers.showToast(
this@SettingsPanel,
"Please select a row to remove",
JOptionPane.WARNING_MESSAGE
)
}
}
).apply {
setFixedSize(40, 30)
}
// Apply button
val applyButton = UiStyler.button(
text = "Apply Changes",
onClick = {
try {
// Commit any active cell editing before reading values
if (table.isEditing) {
table.cellEditor.stopCellEditing()
}
// Get the current HashMap from the field and modify it in place
val currentHashMap = field.get(plugin) as? HashMap<Any, Any> ?: HashMap<Any, Any>()
// Clear the current HashMap
currentHashMap.clear()
// Add the new entries from the table to the existing HashMap
for (i in 0 until tableModel.rowCount) {
val key = tableModel.getValueAt(i, 0).toString()
val value = tableModel.getValueAt(i, 1).toString()
if (key.isBlank()) {
continue
}
if (value.isBlank()) {
Helpers.showToast(
this@SettingsPanel,
"Skipping entry with empty value for key '$key'",
JOptionPane.WARNING_MESSAGE
)
continue
}
val convertedValue = try {
val fieldTypeName = field.genericType.toString()
when {
fieldTypeName.contains("java.util.HashMap<java.lang.String, java.lang.Integer>") ||
fieldTypeName.contains("HashMap<String, Int>") ||
fieldTypeName.contains("HashMap<String, Integer>") -> {
try {
value.toInt()
} catch (numberFormat: NumberFormatException) {
Helpers.showToast(
this@SettingsPanel,
"Invalid number format for key '$key': '$value'. Using 0 as default.",
JOptionPane.WARNING_MESSAGE
)
0
}
}
fieldTypeName.contains("java.lang.Integer") || fieldTypeName.contains("int") -> value.toInt()
fieldTypeName.contains("java.lang.Double") || fieldTypeName.contains("double") -> value.toDouble()
fieldTypeName.contains("java.lang.Float") || fieldTypeName.contains("float") -> value.toFloat()
fieldTypeName.contains("java.lang.Boolean") || fieldTypeName.contains("boolean") -> value.toBoolean()
else -> value
}
} catch (e: Exception) {
value
}
currentHashMap[key] = convertedValue
}
fieldNotifier.setFieldValue(field, currentHashMap)
Helpers.showToast(
this@SettingsPanel,
"${field.name} updated successfully!"
)
} catch (e: Exception) {
Helpers.showToast(
this@SettingsPanel,
"Failed to update ${field.name}: ${e.message}",
JOptionPane.ERROR_MESSAGE
)
}
}
)
buttonsPanel.add(addButton)
buttonsPanel.add(removeButton)
buttonsPanel.add(Box.createHorizontalStrut(20))
buttonsPanel.add(applyButton)
// Add components to the editor panel in vertical stack
editorPanel.add(titleLabel)
editorPanel.add(Box.createVerticalStrut(5))
editorPanel.add(scrollPane)
editorPanel.add(Box.createVerticalStrut(5))
editorPanel.add(buttonsPanel)
return editorPanel
}
private fun Field.exposedDescription(): String {
val exposedAnnotation = annotations.firstOrNull {
it.annotationClass.simpleName == "Exposed"
} ?: return ""
return try {
val descriptionMethod = exposedAnnotation.annotationClass.java.getMethod("description")
descriptionMethod.invoke(exposedAnnotation) as? String ?: ""
} catch (_: Exception) {
""
}
}
private fun Field.isHashMapType(): Boolean {
return type == HashMap::class.java || type.simpleName == "HashMap"
}
companion object {
var customToolTipWindow: JWindow? = null
private var customToolTipLabel: JLabel? = null
fun showCustomToolTip(text: String, component: JComponent) {
val tooltipFont = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16
val maxWidth = 150
val bgColor = Helpers.colorToHex(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND)
val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor)
// Constrain width via HTML so Swing's renderer wraps the text naturally.
val safeText = text
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
val html = """
<html>
<body style="margin:0; padding:6px; width:${maxWidth}px; color:$textColor; background-color:$bgColor; word-wrap:break-word;">
$safeText
</body>
</html>
""".trimIndent()
val (window, label) = if (customToolTipWindow == null || customToolTipLabel == null) {
val lbl = JLabel().apply {
border = BorderFactory.createLineBorder(Color.BLACK)
isOpaque = true
background = KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
foreground = Color.WHITE
font = tooltipFont
horizontalAlignment = SwingConstants.LEFT
verticalAlignment = SwingConstants.TOP
}
val win = JWindow().apply { contentPane.add(lbl) }
customToolTipWindow = win
customToolTipLabel = lbl
win to lbl
} else {
customToolTipWindow!! to customToolTipLabel!!
}
label.text = html
label.font = tooltipFont
label.preferredSize = null // let HTML + pack decide size from width constraint
window.pack()
// Position the tooltip near the component
val locationOnScreen = component.locationOnScreen
customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15)
customToolTipWindow!!.isVisible = true
}
}
}

View file

@ -0,0 +1,128 @@
package KondoKit.ui.components
import KondoKit.ui.ViewConstants
import KondoKit.plugin.Companion.PROGRESS_BAR_FILL
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.util.setFixedSize
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage
import javax.swing.JPanel
class ToggleSwitch : JPanel() {
private var activated = false
private var switchColor = primaryColor
private var buttonColor = secondaryColor
private var borderColor = WIDGET_COLOR
private var activeSwitch = PROGRESS_BAR_FILL
private var puffer: BufferedImage? = null
private var g: Graphics2D? = null
var onToggleListener: ((Boolean) -> Unit)? = null
init {
isVisible = true
addMouseListener(object : MouseAdapter() {
override fun mouseReleased(arg0: MouseEvent) {
activated = !activated
onToggleListener?.invoke(activated)
repaint()
}
})
cursor = Cursor(Cursor.HAND_CURSOR)
setFixedSize(ViewConstants.TOGGLE_SWITCH_SIZE)
isOpaque = false
}
override fun paint(gr: Graphics) {
if (g == null || puffer?.width != width || puffer?.height != height) {
puffer = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
g = puffer?.createGraphics()
val rh = RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
g?.setRenderingHints(rh)
}
g?.color = Color(0, 0, 0, 0)
g?.fillRect(0, 0, width, height)
val trackHeight = height - 2
val trackWidth = width - 2
val trackArc = trackHeight
g?.color = if (activated) activeSwitch else switchColor
g?.fillRoundRect(1, 1, trackWidth, trackHeight, trackArc, trackArc)
g?.color = borderColor
g?.drawRoundRect(1, 1, trackWidth, trackHeight, trackArc, trackArc)
val thumbDiameter = trackHeight - 4
val thumbX = if (activated) {
width - thumbDiameter - 3 // Right side when activated
} else {
3 // Left side when not activated
}
val thumbY = (height - thumbDiameter) / 2
g?.color = buttonColor
g?.fillOval(thumbX, thumbY, thumbDiameter, thumbDiameter)
g?.color = borderColor
g?.drawOval(thumbX, thumbY, thumbDiameter, thumbDiameter)
gr.drawImage(puffer, 0, 0, null)
}
fun isActivated(): Boolean {
return activated
}
fun setActivated(activated: Boolean) {
this.activated = activated
repaint()
}
fun getSwitchColor(): Color {
return switchColor
}
/**
* Unactivated Background Color of switch
*/
fun setSwitchColor(switchColor: Color) {
this.switchColor = switchColor
}
fun getButtonColor(): Color {
return buttonColor
}
/**
* Switch-Button color
*/
fun setButtonColor(buttonColor: Color) {
this.buttonColor = buttonColor
}
fun getBorderColor(): Color {
return borderColor
}
/**
* Border-color of whole switch and switch-button
*/
fun setBorderColor(borderColor: Color) {
this.borderColor = borderColor
}
fun getActiveSwitch(): Color {
return activeSwitch
}
fun setActiveSwitch(activeSwitch: Color) {
this.activeSwitch = activeSwitch
}
}

View file

@ -0,0 +1,74 @@
package KondoKit.ui.components
import KondoKit.ui.ViewConstants
import KondoKit.plugin.Companion.POPUP_BACKGROUND
import KondoKit.plugin.Companion.POPUP_FOREGROUND
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
import KondoKit.plugin.Companion.secondaryColor
import java.awt.Color
import java.awt.Font
import java.awt.Insets
import javax.swing.AbstractButton
import javax.swing.Icon
import javax.swing.JButton
import javax.swing.JMenuItem
/**
* Centralises common Swing styling so buttons and menu items share a
* consistent look and the setup code remains in one place.
*/
object UiStyler {
data class ButtonDefaults(
val background: Color = TITLE_BAR_COLOR,
val foreground: Color = secondaryColor,
val font: Font = ViewConstants.FONT_RUNESCAPE_SMALL_14,
val margin: Insets? = null
)
data class MenuItemDefaults(
val background: Color = POPUP_BACKGROUND,
val foreground: Color = POPUP_FOREGROUND,
val font: Font = ViewConstants.FONT_RUNESCAPE_SMALL_16
)
fun <T : AbstractButton> T.applyDefaults(
defaults: ButtonDefaults = ButtonDefaults(),
onClick: (() -> Unit)? = null
): T {
background = defaults.background
foreground = defaults.foreground
font = defaults.font
defaults.margin?.let { margin = it }
onClick?.let { addActionListener { _ -> it() } }
return this
}
fun button(
text: String? = null,
icon: Icon? = null,
defaults: ButtonDefaults = ButtonDefaults(),
onClick: (() -> Unit)? = null
): JButton {
return JButton().apply {
text?.let { this.text = it }
icon?.let { this.icon = it }
isOpaque = false
applyDefaults(defaults, onClick)
}
}
fun menuItem(
text: String,
defaults: MenuItemDefaults = MenuItemDefaults(),
onClick: (() -> Unit)? = null
): JMenuItem {
return JMenuItem(text).apply {
background = defaults.background
foreground = defaults.foreground
font = defaults.font
onClick?.let { addActionListener { _ -> it() } }
}
}
}

View file

@ -0,0 +1,33 @@
package KondoKit.ui.components
import KondoKit.ui.ViewConstants
import KondoKit.util.setFixedSize
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
import KondoKit.plugin.Companion.secondaryColor
import java.awt.*
import javax.swing.*
class ViewHeader(
title: String,
private val headerHeight: Int = 40
) : JPanel() {
private val titleLabel = JLabel(title)
init {
background = TITLE_BAR_COLOR
setFixedSize(Int.MAX_VALUE, headerHeight)
border = BorderFactory.createEmptyBorder(5, 10, 5, 10)
layout = BorderLayout()
titleLabel.foreground = secondaryColor
titleLabel.font = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16
titleLabel.horizontalAlignment = SwingConstants.CENTER
add(titleLabel, BorderLayout.CENTER)
}
fun setTitle(title: String) {
titleLabel.text = title
}
}

View file

@ -0,0 +1,43 @@
package KondoKit.ui.components
import KondoKit.ui.ViewConstants
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.util.setFixedSize
import java.awt.BorderLayout
import java.awt.Dimension
import javax.swing.BorderFactory
import javax.swing.JPanel
class WidgetPanel(
private val widgetWidth: Int = ViewConstants.DEFAULT_WIDGET_SIZE.width,
private val widgetHeight: Int = ViewConstants.DEFAULT_WIDGET_SIZE.height,
private val addDefaultPadding: Boolean = true,
private val paddingTop: Int? = null,
private val paddingLeft: Int? = null,
private val paddingBottom: Int? = null,
private val paddingRight: Int? = null
) : JPanel() {
init {
layout = BorderLayout(
if (addDefaultPadding) 5 else 0,
if (addDefaultPadding) 5 else 0
)
background = WIDGET_COLOR
val size = Dimension(widgetWidth, widgetHeight)
setFixedSize(size)
val top = paddingTop ?: if (addDefaultPadding) 5 else 0
val left = paddingLeft ?: if (addDefaultPadding) 5 else 0
val bottom = paddingBottom ?: if (addDefaultPadding) 5 else 0
val right = paddingRight ?: if (addDefaultPadding) 5 else 0
border = BorderFactory.createEmptyBorder(top, left, bottom, right)
}
fun setFixedSize(width: Int, height: Int) {
val size = Dimension(width, height)
this@WidgetPanel.setFixedSize(size)
}
}

View file

@ -0,0 +1,20 @@
package KondoKit.ui.components
import java.awt.Container
import javax.swing.JLabel
/**
* Shared widget state used by XP-related features so the structure isn't duplicated.
*/
data class XPWidget(
val container: Container,
val skillId: Int,
val xpGainedLabel: JLabel,
val xpLeftLabel: JLabel,
val xpPerHourLabel: JLabel,
val actionsRemainingLabel: JLabel,
val progressBar: ProgressBar,
var totalXpGained: Int = 0,
var startTime: Long = System.currentTimeMillis(),
var previousXp: Int = 0
)

View file

@ -1,4 +1,4 @@
package KondoKit
package KondoKit.ui.theme
import java.awt.Color
@ -75,7 +75,6 @@ object Themes {
}
}
data class Theme(
val widgetColor: Color,
val titleBarColor: Color,

View file

@ -1,4 +1,4 @@
package KondoKit
package KondoKit.util
import KondoKit.plugin.Companion.FIXED_HEIGHT
import KondoKit.plugin.Companion.FIXED_WIDTH
@ -163,4 +163,4 @@ class AltCanvas : Canvas() {
dispose()
}
}
}
}

View file

@ -0,0 +1,39 @@
package KondoKit.util
import java.awt.Component
import java.awt.Container
import java.awt.Dimension
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JPopupMenu
import KondoKit.util.Helpers
fun <T : Component> T.setFixedSize(width: Int, height: Int): T =
setFixedSize(Dimension(width, height))
fun <T : Component> T.setFixedSize(size: Dimension): T {
preferredSize = size
minimumSize = size
maximumSize = size
return this
}
fun Component.attachPopupMenu(popupMenu: JPopupMenu, includeChildren: Boolean = false) {
val listener = object : MouseAdapter() {
private fun maybeShow(e: MouseEvent) {
if (e.isPopupTrigger) {
popupMenu.show(e.component, e.x, e.y)
}
}
override fun mousePressed(e: MouseEvent) = maybeShow(e)
override fun mouseReleased(e: MouseEvent) = maybeShow(e)
}
addMouseListener(listener)
if (includeChildren && this is Container) {
Helpers.addMouseListenerToAll(this, listener)
}
}

View file

@ -0,0 +1,16 @@
package KondoKit.util
import java.io.File
object FileUtils {
fun deleteRecursively(file: File): Boolean {
if (file.isDirectory) {
file.listFiles()?.forEach { child ->
if (!deleteRecursively(child)) {
return false
}
}
}
return file.delete()
}
}

View file

@ -0,0 +1,50 @@
package KondoKit.util
/**
* Shared loader for GE price JSON from remote or bundled sources.
*/
object GEPriceService {
private const val REMOTE_GE_URL = "https://cdn.2009scape.org/gedata/latest.json"
private data class RemoteGEItem(
val item_id: String?,
val value: String?
)
private data class LocalGEItem(
val id: String?,
val grand_exchange_price: String?
)
private fun <T> mapToPriceMap(
items: Array<T>,
idSelector: (T) -> String?,
priceSelector: (T) -> String?
): Map<String, String> {
return items.mapNotNull { item ->
val id = idSelector(item)
val price = priceSelector(item)
if (id != null && price != null) id to price else null
}.toMap()
}
fun loadRemotePrices(): Map<String, String> {
return try {
val payload = HttpFetcher.fetchString(REMOTE_GE_URL)
val items = JsonParser.fromJson<Array<RemoteGEItem>>(payload)
mapToPriceMap(items, { it.item_id }, { it.value })
} catch (e: Exception) {
emptyMap()
}
}
fun loadLocalPrices(jsonProvider: () -> String?): Map<String, String> {
return try {
val json = jsonProvider() ?: return emptyMap()
val items = JsonParser.fromJson<Array<LocalGEItem>>(json)
mapToPriceMap(items, { it.id }, { it.grand_exchange_price })
} catch (e: Exception) {
emptyMap()
}
}
}

View file

@ -1,8 +1,12 @@
package KondoKit
package KondoKit.util
import KondoKit.plugin
import rt4.GameShell
import java.awt.*
import java.awt.image.BufferedImage
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 +15,44 @@ import java.util.Timer
import javax.swing.*
object Helpers {
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() }
}
}
data class ImageCanvasComponents(val canvas: ImageCanvas, val container: JPanel)
fun createImageCanvasComponents(
bufferedImage: BufferedImage,
background: Color = plugin.WIDGET_COLOR,
size: Dimension = Dimension(bufferedImage.width, bufferedImage.height),
borderInsets: Insets = Insets(-2, 0, 0, 2),
componentPosition: String = BorderLayout.NORTH
): ImageCanvasComponents {
val imageCanvas = ImageCanvas(bufferedImage).apply {
setFixedSize(size)
this.size = size
this.background = background
}
val container = JPanel(BorderLayout()).apply {
this.background = background
add(imageCanvas, componentPosition)
border = BorderFactory.createEmptyBorder(
borderInsets.top,
borderInsets.left,
borderInsets.bottom,
borderInsets.right
)
}
return ImageCanvasComponents(imageCanvas, container)
}
fun convertValue(type: Class<*>, genericType: Type?, value: String): Any {
return when {
@ -233,4 +275,4 @@ object Helpers {
else -> Color(128, 128, 128) // Default grey for unhandled skill IDs
}
}
}
}

View file

@ -0,0 +1,57 @@
package KondoKit.util
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
object HttpFetcher {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
private val defaultHeaders = mapOf(
"User-Agent" to DEFAULT_USER_AGENT,
"Accept" to "*/*"
)
fun openGetConnection(
url: String,
headers: Map<String, String> = emptyMap(),
connectTimeoutMillis: Int? = null,
readTimeoutMillis: Int? = null
): HttpURLConnection {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
(defaultHeaders + headers).forEach { (key, value) ->
connection.setRequestProperty(key, value)
}
connectTimeoutMillis?.let { connection.connectTimeout = it }
readTimeoutMillis?.let { connection.readTimeout = it }
return connection
}
fun fetchString(
url: String,
headers: Map<String, String> = emptyMap(),
connectTimeoutMillis: Int? = null,
readTimeoutMillis: Int? = null
): String {
val connection = openGetConnection(url, headers, connectTimeoutMillis, readTimeoutMillis)
val responseCode = connection.responseCode
val responseStream = if (responseCode in 200..299) {
connection.inputStream
} else {
connection.errorStream
} ?: throw IOException("No response stream available for $url (HTTP $responseCode)")
val payload = responseStream.bufferedReader().use { it.readText() }
if (responseCode !in 200..299) {
throw HttpStatusException(url, responseCode, payload)
}
return payload
}
}
class HttpStatusException(
val url: String,
val statusCode: Int,
val body: String
) : IOException("Request to $url failed with HTTP $statusCode")

View file

@ -1,4 +1,4 @@
package KondoKit
package KondoKit.util
import KondoKit.plugin.Companion.WIDGET_COLOR
import java.awt.Canvas
@ -35,4 +35,4 @@ class ImageCanvas(private val image: BufferedImage) : Canvas() {
override fun getPreferredSize(): Dimension {
return Dimension(image.width, image.height)
}
}
}

View file

@ -0,0 +1,17 @@
package KondoKit.util
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
object JsonParser {
val gson: Gson = Gson()
inline fun <reified T> fromJson(json: String): T {
val type = object : TypeToken<T>() {}.type
return gson.fromJson(json, type)
}
fun <T> fromJson(json: String, classOfT: Class<T>): T = gson.fromJson(json, classOfT)
fun toJson(src: Any): String = gson.toJson(src)
}

View file

@ -1,4 +1,4 @@
package KondoKit
package KondoKit.util
import rt4.GlIndexedSprite
import rt4.GlSprite
@ -7,7 +7,6 @@ import rt4.SoftwareSprite
import java.awt.Color
import java.awt.image.BufferedImage
// Define interfaces for common sprite types
interface BaseSprite {
val width: Int
val height: Int
@ -22,7 +21,6 @@ interface NonIndexedSprite : BaseSprite {
val pixels: IntArray?
}
// Adapter functions for existing sprite types
fun adaptSoftwareSprite(sprite: SoftwareSprite): NonIndexedSprite {
return object : NonIndexedSprite {
override val width: Int = sprite.width
@ -196,4 +194,4 @@ object SpriteToBufferedImage {
else -> BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) // Default empty image for unsupported types
}
}
}
}

View file

@ -0,0 +1,28 @@
package KondoKit.util
object XPTable {
private val xpForLevels: IntArray = IntArray(100)
init {
var points = 0.0
for (level in 1..99) {
points += Math.floor(level + 300.0 * Math.pow(2.0, level / 7.0))
xpForLevels[level] = Math.floor(points / 4).toInt()
}
}
fun getLevelForXp(xp: Int): Pair<Int, Int> {
for (i in 1..99) {
if (xp < xpForLevels[i]) {
return Pair(i - 1, xp - xpForLevels[i - 1])
}
}
return Pair(99, 0)
}
fun getXpRequiredForLevel(level: Int): Int {
if (level <= 0) return 0
if (level >= 99) return xpForLevels[99]
return xpForLevels[level]
}
}

View file

@ -0,0 +1,383 @@
package KondoKit.views
import KondoKit.util.Helpers.getSpriteId
import KondoKit.util.Helpers.showToast
import KondoKit.util.ImageCanvas
import KondoKit.ui.BaseView
import KondoKit.ui.ViewConstants
import KondoKit.ui.View
import KondoKit.util.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.ui.components.LabelComponent
import KondoKit.ui.components.WidgetPanel
import KondoKit.ui.components.SearchField
import KondoKit.views.ViewLayoutHelpers.createSearchFieldSection
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.POPUP_FOREGROUND
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
import KondoKit.util.JsonParser
import plugin.api.API
import rt4.Sprites
import java.awt.*
import KondoKit.util.HttpFetcher
import KondoKit.util.HttpStatusException
import java.net.SocketTimeoutException
import javax.swing.*
import javax.swing.border.MatteBorder
import kotlin.math.floor
object HiscoresView : View {
const val VIEW_NAME = "HISCORE_SEARCH_VIEW"
var hiScoreView: JPanel? = null
var customSearchField: SearchField? = null
override val name: String = VIEW_NAME
override val iconSpriteId: Int = ViewConstants.MAG_SPRITE
override val panel: JPanel
get() = hiScoreView ?: JPanel()
override fun createView() {
createHiscoreSearchView()
}
override fun registerFunctions() {
}
fun createHiscoreSearchView() {
val hiscorePanel = BaseView(VIEW_NAME, addDefaultSpacing = false).apply {
background = VIEW_BACKGROUND_COLOR
setViewSize(ViewConstants.DEFAULT_PANEL_SIZE.height)
}
val searchSection = createSearchFieldSection(
parent = hiscorePanel,
placeholderText = "Search player...",
viewName = VIEW_NAME
) { username ->
searchPlayerForHiscores(username, hiscorePanel)
}
customSearchField = searchSection.searchField
hiscorePanel.add(Box.createVerticalStrut(5))
hiscorePanel.add(searchSection.wrapper)
hiscorePanel.add(Box.createVerticalStrut(5))
val playerNamePanel = WidgetPanel(
widgetWidth = ViewConstants.FILTER_PANEL_SIZE.width,
widgetHeight = ViewConstants.FILTER_PANEL_SIZE.height,
addDefaultPadding = false
).apply {
layout = GridBagLayout()
background = TOOLTIP_BACKGROUND.darker()
name = "playerNameLabel"
}
hiscorePanel.add(playerNamePanel)
hiscorePanel.add(Box.createVerticalStrut(5))
val skillsPanel = WidgetPanel(
widgetWidth = ViewConstants.SKILLS_PANEL_SIZE.width,
widgetHeight = ViewConstants.SKILLS_PANEL_SIZE.height,
addDefaultPadding = false
).apply {
layout = FlowLayout(FlowLayout.CENTER, 0, 0)
background = VIEW_BACKGROUND_COLOR
}
for (i in ViewConstants.SKILL_DISPLAY_ORDER) {
val skillPanel = WidgetPanel(
widgetWidth = ViewConstants.SKILL_PANEL_SIZE.width,
widgetHeight = ViewConstants.SKILL_PANEL_SIZE.height,
addDefaultPadding = false
).apply {
layout = BorderLayout()
background = WIDGET_COLOR
border = MatteBorder(5, 0, 0, 0, WIDGET_COLOR)
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(i)))
val imageCanvas = bufferedImageSprite.let {
ImageCanvas(it).apply {
preferredSize = ViewConstants.SKILL_SPRITE_SIZE
size = ViewConstants.SKILL_SPRITE_SIZE
fillColor = WIDGET_COLOR
}
}
val numberLabel = JLabel(" ", JLabel.RIGHT).apply {
name = "skillLabel_$i"
foreground = POPUP_FOREGROUND
font = ViewConstants.FONT_RUNESCAPE_SMALL_16
preferredSize = ViewConstants.NUMBER_LABEL_SIZE
minimumSize = ViewConstants.NUMBER_LABEL_SIZE
}
val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply {
background = WIDGET_COLOR
add(imageCanvas)
add(numberLabel)
}
skillPanel.add(imageContainer, BorderLayout.CENTER)
skillsPanel.add(skillPanel)
}
hiscorePanel.add(skillsPanel)
val totalCombatPanel = WidgetPanel(
widgetWidth = ViewConstants.TOTAL_COMBAT_PANEL_SIZE.width,
widgetHeight = ViewConstants.TOTAL_COMBAT_PANEL_SIZE.height,
addDefaultPadding = false
).apply {
layout = FlowLayout(FlowLayout.CENTER, 0, 0)
background = WIDGET_COLOR
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(ViewConstants.LVL_BAR_SPRITE))
val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply {
fillColor = WIDGET_COLOR
preferredSize = ViewConstants.DIMENSION_LARGE_ICON
size = ViewConstants.DIMENSION_LARGE_ICON
}
val totalLevelLabel = JLabel(" ").apply {
name = "totalLevelLabel"
foreground = POPUP_FOREGROUND
font = ViewConstants.FONT_ARIAL_BOLD_12
horizontalAlignment = JLabel.LEFT
iconTextGap = 10
}
val totalLevelPanel = WidgetPanel(
widgetWidth = ViewConstants.TOTAL_COMBAT_PANEL_SIZE.width / 2,
widgetHeight = ViewConstants.TOTAL_COMBAT_PANEL_SIZE.height,
addDefaultPadding = false,
paddingLeft = 45
).apply {
layout = FlowLayout(FlowLayout.LEFT, 5, 5)
background = WIDGET_COLOR
add(totalLevelIcon)
add(totalLevelLabel)
}
val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(ViewConstants.COMBAT_LVL_SPRITE))
val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply {
fillColor = WIDGET_COLOR
preferredSize = ViewConstants.DIMENSION_LARGE_ICON
size = ViewConstants.DIMENSION_LARGE_ICON
}
val combatLevelLabel = JLabel("").apply {
name = "combatLevelLabel"
foreground = POPUP_FOREGROUND
font = ViewConstants.FONT_ARIAL_BOLD_12
horizontalAlignment = JLabel.LEFT
iconTextGap = 10
}
val combatLevelPanel = WidgetPanel(
widgetWidth = ViewConstants.TOTAL_COMBAT_PANEL_SIZE.width / 2,
widgetHeight = ViewConstants.TOTAL_COMBAT_PANEL_SIZE.height,
addDefaultPadding = false
).apply {
layout = FlowLayout(FlowLayout.LEFT, 5, 6)
background = WIDGET_COLOR
add(combatLevelIcon)
add(combatLevelLabel)
}
totalCombatPanel.add(totalLevelPanel)
totalCombatPanel.add(combatLevelPanel)
hiscorePanel.add(totalCombatPanel)
hiscorePanel.add(Box.createVerticalStrut(10))
hiScoreView = hiscorePanel
}
data class HiscoresResponse(
val info: PlayerInfo,
val skills: List<Skill>
)
data class PlayerInfo(
val exp_multiplier: String,
val iron_mode: String
)
data class Skill(
val id: String,
val static: String
)
fun searchPlayerForHiscores(username: String, hiscoresPanel: JPanel) {
if(username.isEmpty() || username.length < 3) {
return
}
val cleanUsername = username.replace(" ", "_")
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${cleanUsername.toLowerCase()}"
customSearchField?.setText(username)
updateHiscoresViewStandalone(hiscoresPanel, null, "Searching...")
Thread {
try {
val response = HttpFetcher.fetchString(
url = apiUrl,
connectTimeoutMillis = 5000,
readTimeoutMillis = 5000
)
SwingUtilities.invokeLater {
updatePlayerDataStandalone(response, username, hiscoresPanel)
}
} catch (e: SocketTimeoutException) {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Request timed out", JOptionPane.ERROR_MESSAGE)
}
} catch (e: HttpStatusException) {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Player not found!", JOptionPane.ERROR_MESSAGE)
}
} catch (e: Exception) {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Error fetching data!", JOptionPane.ERROR_MESSAGE)
}
}
}.start()
}
private fun updatePlayerDataStandalone(jsonResponse: String, username: String, hiscoresPanel: JPanel) {
val hiscoresResponse = JsonParser.fromJson<HiscoresResponse>(jsonResponse)
updateHiscoresViewStandalone(hiscoresPanel, hiscoresResponse, username)
}
private fun updateHiscoresViewStandalone(hiscoresPanel: JPanel, data: HiscoresResponse?, username: String) {
val playerNameLabel = findComponentByNameStandalone(hiscoresPanel, "playerNameLabel") as? JPanel
playerNameLabel?.removeAll()
var nameLabel = LabelComponent().apply {
updateHtmlText(username, secondaryColor, "", primaryColor)
font = ViewConstants.FONT_ARIAL_BOLD_12
foreground = POPUP_FOREGROUND
border = BorderFactory.createEmptyBorder(0, 6, 0, 0)
horizontalAlignment = JLabel.CENTER
}
playerNameLabel?.add(nameLabel)
playerNameLabel?.revalidate()
playerNameLabel?.repaint()
if (data == null) return
playerNameLabel?.removeAll()
val ironMode = data.info.iron_mode
if (ironMode != "0") {
val ironmanBufferedImage =
getBufferedImageFromSprite(Sprites.nameIcons[ViewConstants.IRONMAN_SPRITE + ironMode.toInt() - 1])
val imageCanvas = ironmanBufferedImage.let {
ImageCanvas(it).apply {
preferredSize = ViewConstants.IMAGE_CANVAS_SIZE
size = ViewConstants.IMAGE_CANVAS_SIZE
}
}
playerNameLabel?.add(imageCanvas)
}
val exp_multiplier = data.info.exp_multiplier
nameLabel = LabelComponent().apply {
updateHtmlText(username, secondaryColor, " (${exp_multiplier}x)", primaryColor)
font = ViewConstants.FONT_ARIAL_BOLD_12
foreground = POPUP_FOREGROUND
border = BorderFactory.createEmptyBorder(0, 6, 0, 0)
horizontalAlignment = JLabel.CENTER
}
playerNameLabel?.add(nameLabel)
playerNameLabel?.revalidate()
playerNameLabel?.repaint()
data.skills.forEachIndexed { index, skill ->
val labelName = "skillLabel_$index"
val numberLabel = findComponentByNameStandalone(hiscoresPanel, labelName) as? JLabel
numberLabel?.text = skill.static.toInt().toString()
}
updateTotalAndCombatLevelStandalone(data.skills, hiscoresPanel, true)
hiscoresPanel.revalidate()
hiscoresPanel.repaint()
}
private fun updateTotalAndCombatLevelStandalone(
skills: List<Skill>,
hiscoresPanel: JPanel,
isMemberWorld: Boolean
) {
val totalLevel = skills.sumBy { it.static.toInt() }
val totalLevelLabel = findComponentByNameStandalone(hiscoresPanel, "totalLevelLabel") as? JLabel
totalLevelLabel?.text = totalLevel.toString()
val attack = skills.find { it.id == "0" }?.static?.toInt() ?: 1
val defence = skills.find { it.id == "1" }?.static?.toInt() ?: 1
val strength = skills.find { it.id == "2" }?.static?.toInt() ?: 1
val hitpoints = skills.find { it.id == "3" }?.static?.toInt() ?: 1
val ranged = skills.find { it.id == "4" }?.static?.toInt() ?: 1
val prayer = skills.find { it.id == "5" }?.static?.toInt() ?: 1
val magic = skills.find { it.id == "6" }?.static?.toInt() ?: 1
val summoning = skills.find { it.id == "23" }?.static?.toInt() ?: 1
val combatLevel =
calculateCombatLevel(attack, defence, strength, hitpoints, prayer, ranged, magic, summoning, true)
val combatLevelLabel = findComponentByNameStandalone(hiscoresPanel, "combatLevelLabel") as? JLabel
combatLevelLabel?.text = combatLevel.toString()
}
private fun findComponentByNameStandalone(container: Container, name: String): Component? {
for (component in container.components) {
if (name == component.name) {
return component
}
if (component is Container) {
val child = findComponentByNameStandalone(component, name)
if (child != null) {
return child
}
}
}
return null
}
private fun calculateCombatLevel(
attack: Int,
defence: Int,
strength: Int,
hitpoints: Int,
prayer: Int,
ranged: Int,
magic: Int,
summoning: Int,
isMemberWorld: Boolean
): Double {
val base = (defence + hitpoints + floor(prayer.toDouble() / 2)) * 0.25
val melee = (attack + strength) * 0.325
val range = floor(ranged * 1.5) * 0.325
val mage = floor(magic * 1.5) * 0.325
val maxCombatType = maxOf(melee, range, mage)
val summoningFactor = if (isMemberWorld) floor(summoning.toDouble() / 8) else 0.0
return Math.round((base + maxCombatType + summoningFactor) * 1000.0) / 1000.0
}
}

View file

@ -1,38 +1,46 @@
package KondoKit
package KondoKit.views
import KondoKit.Helpers.addMouseListenerToAll
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.plugin.Companion.POPUP_BACKGROUND
import KondoKit.plugin.Companion.POPUP_FOREGROUND
import KondoKit.util.Helpers
import KondoKit.util.Helpers.formatHtmlLabelText
import KondoKit.util.ImageCanvas
import KondoKit.util.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.ui.BaseView
import KondoKit.ui.OnKillingBlowNPCCallback
import KondoKit.ui.OnPostClientTickCallback
import KondoKit.ui.ViewConstants
import KondoKit.ui.View
import KondoKit.util.attachPopupMenu
import KondoKit.ui.components.XPWidget
import KondoKit.util.setFixedSize
import KondoKit.views.XPTrackerView.wrappedWidget
import KondoKit.ui.components.PopupMenuComponent
import KondoKit.ui.components.ProgressBar
import KondoKit.ui.components.LabelComponent
import KondoKit.ui.components.WidgetPanel
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.registerDrawAction
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.Companion.useLiveGEPrices
import KondoKit.plugin.StateManager.focusedView
import plugin.api.API
import rt4.*
import java.awt.*
import java.awt.Font
import java.awt.Component
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import KondoKit.util.GEPriceService
import java.text.DecimalFormat
import javax.swing.*
import kotlin.math.ceil
object LootTrackerView {
object LootTrackerView : View, OnPostClientTickCallback, OnKillingBlowNPCCallback {
private const val SNAPSHOT_LIFESPAN = 10
const val BAG_ICON = 900
const val OPEN_BAG = 777
val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>()
var gePriceMap = loadGEPrices()
const val VIEW_NAME = "LOOT_TRACKER_VIEW"
@ -42,94 +50,50 @@ object LootTrackerView {
var lastConfirmedKillNpcId = -1
private var customToolTipWindow: JWindow? = null
var lootTrackerView: JPanel? = null
override val name: String = VIEW_NAME
override val iconSpriteId: Int = OPEN_BAG
fun loadGEPrices(): Map<String, String> {
return if (plugin.useLiveGEPrices) {
try {
println("LootTracker: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0")
override val panel: JPanel
get() = lootTrackerView ?: JPanel()
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val inputStream = connection.inputStream
val content = inputStream.bufferedReader().use(BufferedReader::readText)
override fun createView() {
createLootTrackerView()
}
val items = content.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
override fun registerFunctions() {
KondoKit.plugin.registerPostClientTickCallback(this)
KondoKit.plugin.registerKillingBlowNPCCallback(this)
}
for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",")
val itemId = pairs.find { it.trim().startsWith("\"item_id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val value = pairs.find { it.trim().startsWith("\"value\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (itemId != null && value != null) {
gePrices[itemId] = value
}
}
override fun onPostClientTick() {
lootTrackerView?.let { onPostClientTick(it) }
}
gePrices
} else {
emptyMap()
}
} catch (e: Exception) {
emptyMap()
}
override fun onKillingBlowNPC(npcID: Int, x: Int, z: Int) {
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
npcDeathSnapshots[npcID] = GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
}
fun loadGEPrices(): Map<String, String> {
return if (useLiveGEPrices) {
println("LootTracker: Loading Remote GE Prices")
GEPriceService.loadRemotePrices()
} 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<String, String>()
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
}
} catch (e: Exception) {
emptyMap()
}
println("LootTracker: Loading Local GE Prices")
GEPriceService.loadLocalPrices { Helpers.readResourceText("res/item_configs.json") }
}
}
fun createLootTrackerView() {
lootTrackerView = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS) // Use BoxLayout on Y axis to stack widgets vertically
background = VIEW_BACKGROUND_COLOR
lootTrackerView = BaseView(VIEW_NAME, addDefaultSpacing = false).apply {
add(Box.createVerticalStrut(5))
totalTrackerWidget = createTotalLootWidget()
val wrapped = wrappedWidget(totalTrackerWidget!!.container)
val wrapped = wrappedWidget(totalTrackerWidget!!.container, padding = 0)
val popupMenu = resetLootTrackerMenu()
// Create a custom MouseListener
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)
}
}
}
addMouseListenerToAll(wrapped,rightClickListener)
wrapped.addMouseListener(rightClickListener)
wrapped.attachPopupMenu(popupMenu, includeChildren = true)
add(wrapped)
add(Box.createVerticalStrut(8))
revalidate()
@ -172,42 +136,41 @@ object LootTrackerView {
)
}
private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): Panel {
val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
minimumSize = preferredSize
maximumSize = preferredSize
size = preferredSize
background = WIDGET_COLOR
}
private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): WidgetPanel {
val (_, imageContainer) = Helpers.createImageCanvasComponents(
bufferedImageSprite,
borderInsets = Insets(-2, 0, 0, 0)
)
val imageContainer = Panel(BorderLayout()).apply {
background = WIDGET_COLOR
add(imageCanvas, BorderLayout.NORTH)
}
return Panel(BorderLayout(5, 0)).apply {
background = WIDGET_COLOR
preferredSize = TOTAL_XP_WIDGET_SIZE
return WidgetPanel(
widgetWidth = ViewConstants.DEFAULT_WIDGET_SIZE.width,
widgetHeight = ViewConstants.TOTAL_XP_WIDGET_SIZE.height,
addDefaultPadding = false,
paddingTop = 10,
paddingBottom = 10,
paddingRight = 10,
paddingLeft = 10
).apply {
layout = BorderLayout(5, 0)
setFixedSize(
ViewConstants.DEFAULT_WIDGET_SIZE.width,
ViewConstants.TOTAL_XP_WIDGET_SIZE.height
)
add(imageContainer, BorderLayout.WEST)
add(createTextPanel(l1,l2), BorderLayout.CENTER)
}
}
private fun createTextPanel(l1 : JLabel, l2: JLabel): Panel {
return Panel(GridLayout(2, 1, 5, 0)).apply {
private fun createTextPanel(l1 : JLabel, l2: JLabel): JPanel {
return JPanel(GridLayout(2, 1, 5, 0)).apply {
background = WIDGET_COLOR
add(l1)
add(l2)
}
}
private fun createLabel(text: String): JLabel {
return JLabel(text).apply {
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
horizontalAlignment = JLabel.LEFT
}
}
private fun createLabel(text: String): LabelComponent =
LabelComponent(text, alignment = JLabel.LEFT)
private fun addItemToLootPanel(lootTrackerView: JPanel, drop: Item, npcName: String) {
findLootItemsPanel(lootTrackerView, npcName)?.let { lootPanel ->
@ -218,15 +181,12 @@ object LootTrackerView {
?.let { updateItemPanelIcon(it, drop.id, newQuantity) }
?: addNewItemToPanel(lootPanel, drop, newQuantity)
// Recalculate lootPanel size based on the number of unique items.
val totalItems = lootItemPanels[npcName]?.size ?: 0
val rowsNeeded = ceil(totalItems / 6.0).toInt()
val lootPanelHeight = rowsNeeded * (40)
val size = Dimension(lootPanel.width,lootPanelHeight+32)
lootPanel.parent.preferredSize = size
lootPanel.parent.minimumSize = size
lootPanel.parent.maximumSize = size
val size = Dimension(lootPanel.width, lootPanelHeight + 32)
(lootPanel.parent as? Component)?.setFixedSize(size)
lootPanel.parent.revalidate()
lootPanel.parent.repaint()
lootPanel.revalidate()
@ -244,35 +204,24 @@ object LootTrackerView {
private fun createItemPanel(itemId: Int, quantity: Int): JPanel {
val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 1, 3153952))
// Create the panel for the item
val itemPanel = FixedSizePanel(Dimension(36, 32)).apply {
preferredSize = Dimension(36, 32)
background = WIDGET_COLOR
minimumSize = preferredSize
maximumSize = preferredSize
val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
preferredSize = Dimension(36, 32)
setFixedSize(36, 32)
background = WIDGET_COLOR
minimumSize = preferredSize
maximumSize = preferredSize
}
// Add the imageCanvas to the panel
add(imageCanvas, BorderLayout.CENTER)
// Put the itemId as a property for reference
putClientProperty("itemId", itemId)
// Add mouse listener for custom hover text
imageCanvas.addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
// Show custom tooltip when the mouse enters the component
showCustomToolTip(e.point, itemId,quantity,imageCanvas)
}
override fun mouseExited(e: MouseEvent) {
// Hide tooltip when mouse exits
hideCustomToolTip()
}
})
@ -281,7 +230,6 @@ object LootTrackerView {
return itemPanel
}
// Function to show the custom tooltip
fun showCustomToolTip(location: Point, itemId: Int, quantity: Int, parentComponent: ImageCanvas) {
val itemDef = ObjTypeList.get(itemId)
val gePricePerItem = gePriceMap[itemDef.id.toString()]?.toInt() ?: 0
@ -296,7 +244,7 @@ object LootTrackerView {
"GE: ${formatValue(totalGePrice)} ${geText}<br>" +
"HA: ${formatValue(totalHaPrice)} ${haText}</div></html>"
val _font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val tooltipFont = ViewConstants.FONT_RUNESCAPE_SMALL_16
if (customToolTipWindow == null) {
customToolTipWindow = JWindow().apply {
contentPane = JLabel(text).apply {
@ -304,19 +252,17 @@ object LootTrackerView {
isOpaque = true
background = TOOLTIP_BACKGROUND
foreground = Color.WHITE
font = _font
font = tooltipFont
}
pack()
}
}
// Calculate the tooltip location relative to the parent component
val screenLocation = parentComponent.locationOnScreen
customToolTipWindow!!.setLocation(screenLocation.x + location.x, screenLocation.y + location.y + 20)
customToolTipWindow!!.isVisible = true
}
// Function to hide the custom tooltip
fun hideCustomToolTip() {
customToolTipWindow?.isVisible = false
customToolTipWindow = null // Nullify the global instance
@ -376,7 +322,8 @@ object LootTrackerView {
fun onPostClientTick(lootTrackerView: JPanel) {
val toRemove = mutableListOf<Int>()
npcDeathSnapshots.entries.forEach { (npcId, snapshot) ->
npcDeathSnapshots.entries.forEach { entry ->
val (npcId, snapshot) = entry
val postDeathSnapshot = takeGroundSnapshot(Pair(snapshot.location.first, snapshot.location.second))
val newDrops = postDeathSnapshot.subtract(snapshot.items)
@ -392,7 +339,9 @@ object LootTrackerView {
}
}
toRemove.forEach { npcDeathSnapshots.remove(it) }
toRemove.forEach { npcId ->
npcDeathSnapshots.remove(npcId)
}
}
@ -427,7 +376,7 @@ object LootTrackerView {
newDrops.forEach { drop ->
val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity
updateValueLabel(lootTrackerView, geValue.toString(), npcName)
plugin.registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
updateTotalValue(geValue)
}
}
@ -436,30 +385,28 @@ object LootTrackerView {
val childFramePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR
minimumSize = Dimension(230, 0)
maximumSize = Dimension(230, 700)
minimumSize = Dimension(ViewConstants.DEFAULT_WIDGET_SIZE.width, 0)
maximumSize = Dimension(ViewConstants.DEFAULT_WIDGET_SIZE.width, 700)
name = "HELLO_WORLD"
}
val labelPanel = JPanel(BorderLayout()).apply {
background = TITLE_BAR_COLOR
border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
maximumSize = Dimension(230, 24)
minimumSize = maximumSize
preferredSize = maximumSize
setFixedSize(ViewConstants.DEFAULT_WIDGET_SIZE.width, 24)
}
val killCount = npcKillCounts.getOrPut(npcName) { 0 }
val countLabel = JLabel(formatHtmlLabelText(npcName, secondaryColor, " x $killCount", primaryColor)).apply {
foreground = secondaryColor
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
font = ViewConstants.FONT_RUNESCAPE_SMALL_16
horizontalAlignment = JLabel.LEFT
name = "killCountLabel_$npcName"
}
val valueLabel = JLabel("0 gp").apply {
foreground = secondaryColor
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
font = ViewConstants.FONT_RUNESCAPE_SMALL_16
horizontalAlignment = JLabel.RIGHT
name = "valueLabel_$npcName"
}
@ -467,7 +414,6 @@ object LootTrackerView {
labelPanel.add(countLabel, BorderLayout.WEST)
labelPanel.add(valueLabel, BorderLayout.EAST)
// Panel to hold loot items, using GridLayout to manage rows and columns.
val lootPanel = JPanel().apply {
background = WIDGET_COLOR
border = BorderFactory.createLineBorder(WIDGET_COLOR, 5)
@ -481,41 +427,13 @@ object LootTrackerView {
childFramePanel.add(lootPanel)
val popupMenu = removeLootFrameMenu(childFramePanel, npcName)
// Create a custom MouseListener
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)
}
}
}
labelPanel.addMouseListener(rightClickListener)
labelPanel.attachPopupMenu(popupMenu)
return childFramePanel
}
private fun removeLootFrameMenu(toRemove: JPanel, npcName: String): JPopupMenu {
// Create a popup menu
val popupMenu = JPopupMenu()
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
popupMenu.background = POPUP_BACKGROUND
// Create menu items with custom font and colors
val menuItem1 = JMenuItem("Remove").apply {
font = rFont // Set custom font
background = POPUP_BACKGROUND // Dark background for item
foreground = POPUP_FOREGROUND // Light text color for item
}
popupMenu.add(menuItem1)
menuItem1.addActionListener {
val popupMenu = PopupMenuComponent()
popupMenu.addMenuItem("Remove") {
lootItemPanels[npcName]?.clear()
npcKillCounts[npcName] = 0
lootTrackerView?.let { parent ->
@ -524,8 +442,6 @@ object LootTrackerView {
if (toRemoveIndex >= 0 && toRemoveIndex < components.size - 1) {
val nextComponent = components[toRemoveIndex + 1]
if (nextComponent is Box.Filler) {
// Nasty way to remove the Box.createVerticalStrut(8) after
// the lootpanel.
parent.remove(nextComponent)
}
}
@ -539,21 +455,10 @@ object LootTrackerView {
private fun resetLootTrackerMenu(): JPopupMenu {
// Create a popup menu
val popupMenu = JPopupMenu()
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val popupMenu = PopupMenuComponent()
popupMenu.background = POPUP_BACKGROUND
// Create menu items with custom font and colors
val menuItem1 = JMenuItem("Reset Loot Tracker").apply {
font = rFont // Set custom font
background = POPUP_BACKGROUND // Dark background for item
foreground = POPUP_FOREGROUND // Light text color for item
}
popupMenu.add(menuItem1)
menuItem1.addActionListener {
plugin.registerDrawAction {
popupMenu.addMenuItem("Reset Loot Tracker") {
registerDrawAction {
resetLootTracker()
}
}
@ -566,25 +471,9 @@ object LootTrackerView {
lootItemPanels.clear()
totalTrackerWidget = createTotalLootWidget()
val wrapped = wrappedWidget(totalTrackerWidget!!.container)
val wrapped = wrappedWidget(totalTrackerWidget!!.container, padding = 0)
val _popupMenu = resetLootTrackerMenu()
// Create a custom MouseListener
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)
}
}
}
addMouseListenerToAll(wrapped,rightClickListener)
wrapped.addMouseListener(rightClickListener)
wrapped.attachPopupMenu(_popupMenu, includeChildren = true)
lootTrackerView?.add(Box.createVerticalStrut(5))
lootTrackerView?.add(wrapped)
lootTrackerView?.add(Box.createVerticalStrut(8))

View file

@ -0,0 +1,760 @@
package KondoKit.views
import KondoKit.pluginmanager.ReflectiveEditorPlugin
import KondoKit.util.attachPopupMenu
import KondoKit.ui.components.*
import KondoKit.pluginmanager.PluginInfoWithStatus
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.WRENCH_ICON
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.util.setFixedSize
import KondoKit.ui.BaseView
import KondoKit.ui.View
import KondoKit.ui.ViewConstants
import KondoKit.util.Helpers.showToast
import KondoKit.views.ViewLayoutHelpers.createSearchFieldSection
import plugin.Plugin
import plugin.PluginInfo
import plugin.PluginRepository
import java.awt.*
import java.io.File
import java.util.*
import javax.swing.*
import kotlin.math.ceil
/*
This is used for the runtime editing of plugin variables.
To expose fields add the @Exposed annotation.
When they are applied this will trigger an invoke of OnKondoValueUpdated()
if it is implemented. Check GroundItems plugin for an example.
*/
object ReflectiveEditorView : View {
var reflectiveEditorView: JPanel? = null
const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW"
const val PLUGIN_LIST_VIEW = "PLUGIN_LIST"
const val PLUGIN_DETAIL_VIEW = "PLUGIN_DETAIL"
private val reflectiveEditorPlugin = ReflectiveEditorPlugin()
private var searchField: SearchField? = null
private fun isKondoKit(pluginName: String): Boolean {
return pluginName == "KondoKit"
}
private lateinit var cardLayout: CardLayout
private lateinit var mainPanel: JPanel
private var currentPluginInfo: PluginInfo? = null
private var currentPlugin: Plugin? = null
private var pluginSearchText: String = ""
private var searchFieldWrapper: JPanel? = null
private var pluginListContentPanel: JPanel? = null
private var pluginListScrollablePanel: ScrollablePanel? = null
override val name: String = VIEW_NAME
override val iconSpriteId: Int = WRENCH_ICON
override val panel: JPanel
get() = reflectiveEditorView ?: JPanel()
override fun createView() {
reflectiveEditorPlugin.Init()
createReflectiveEditorView()
}
override fun registerFunctions() {
}
fun createReflectiveEditorView() {
cardLayout = CardLayout()
mainPanel = JPanel(cardLayout)
mainPanel.background = VIEW_BACKGROUND_COLOR
mainPanel.border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
mainPanel.isDoubleBuffered = true
val pluginListView = createPluginListView()
pluginListView.name = PLUGIN_LIST_VIEW
mainPanel.add(pluginListView, PLUGIN_LIST_VIEW)
val pluginDetailView = BaseView(PLUGIN_DETAIL_VIEW).apply {
layout = BorderLayout()
background = VIEW_BACKGROUND_COLOR
}
mainPanel.add(pluginDetailView, PLUGIN_DETAIL_VIEW)
reflectiveEditorView = mainPanel
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
}
private fun createPluginListView(): JPanel {
val panel = BaseView(PLUGIN_LIST_VIEW, addDefaultSpacing = false).apply {
background = VIEW_BACKGROUND_COLOR
}
val searchSection = createSearchFieldSection(
parent = panel,
placeholderText = "Search plugins...",
viewName = VIEW_NAME,
initialText = pluginSearchText
) { searchText ->
pluginSearchText = searchText
SwingUtilities.invokeLater {
addPlugins(reflectiveEditorView!!)
}
}
this.searchField = searchSection.searchField
searchFieldWrapper = searchSection.wrapper
panel.add(Box.createVerticalStrut(5))
panel.add(searchSection.wrapper)
panel.add(Box.createVerticalStrut(10))
pluginListContentPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = VIEW_BACKGROUND_COLOR
alignmentX = Component.CENTER_ALIGNMENT
}
panel.add(pluginListContentPanel)
val scrollablePanel = ScrollablePanel(panel)
pluginListScrollablePanel = scrollablePanel
val container = JPanel(BorderLayout())
container.background = VIEW_BACKGROUND_COLOR
container.add(scrollablePanel, BorderLayout.CENTER)
populatePluginListContent()
SwingUtilities.invokeLater {
resetScrollablePanel(scrollablePanel)
}
return container
}
private fun populatePluginListContent() {
val contentPanel = pluginListContentPanel ?: return
val treeLock = contentPanel.treeLock
synchronized(treeLock) {
contentPanel.removeAll()
try {
val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins")
loadedPluginsField.isAccessible = true
val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *>
val pluginsWithExposed = mutableListOf<Pair<PluginInfo, Plugin>>()
val pluginsWithoutExposed = mutableListOf<Pair<PluginInfo, Plugin>>()
for ((pluginInfo, plugin) in loadedPlugins) {
val exposedFields = (plugin as Plugin).javaClass.declaredFields.filter { field ->
field.annotations.any { annotation ->
annotation.annotationClass.simpleName == "Exposed"
}
}
val pluginName = plugin.javaClass.`package`.name
if (pluginSearchText.isBlank() || pluginName.contains(pluginSearchText, ignoreCase = true)) {
if (exposedFields.isNotEmpty()) {
pluginsWithExposed.add(pluginInfo as PluginInfo to plugin)
} else {
pluginsWithoutExposed.add(pluginInfo as PluginInfo to plugin)
}
}
}
pluginsWithExposed.sortWith(compareBy(
{ if (isKondoKit(it.second.javaClass.`package`.name)) 0 else 1 },
{ it.second.javaClass.`package`.name }
))
pluginsWithoutExposed.sortWith(compareBy(
{ if (isKondoKit(it.second.javaClass.`package`.name)) 0 else 1 },
{ it.second.javaClass.`package`.name }
))
for ((pluginInfo, plugin) in pluginsWithExposed) {
val pluginPanel = createPluginItemPanel(pluginInfo, plugin)
contentPanel.add(pluginPanel)
contentPanel.add(Box.createVerticalStrut(5))
}
if (pluginsWithExposed.isNotEmpty() && pluginsWithoutExposed.isNotEmpty()) {
val separator = JPanel()
separator.background = VIEW_BACKGROUND_COLOR
separator.preferredSize = Dimension(Int.MAX_VALUE, 1)
separator.maximumSize = Dimension(Int.MAX_VALUE, 1)
contentPanel.add(separator)
contentPanel.add(Box.createVerticalStrut(5))
}
for ((pluginInfo, plugin) in pluginsWithoutExposed) {
val pluginPanel = createPluginItemPanel(pluginInfo, plugin)
contentPanel.add(pluginPanel)
contentPanel.add(Box.createVerticalStrut(5))
}
} catch (e: Exception) {
e.printStackTrace()
}
val disabledDir = File(reflectiveEditorPlugin.getPluginsDirectory(), "disabled")
if (disabledDir.exists() && disabledDir.isDirectory) {
val disabledPlugins = disabledDir.listFiles { file -> file.isDirectory } ?: arrayOf()
for (pluginDir in disabledPlugins.sortedBy { it.name }) {
if (isKondoKit(pluginDir.name)) {
continue
}
if (pluginSearchText.isBlank() || pluginDir.name.contains(pluginSearchText, ignoreCase = true)) {
val pluginPanel = createDisabledPluginItemPanel(pluginDir.name)
contentPanel.add(pluginPanel)
contentPanel.add(Box.createVerticalStrut(5))
}
}
}
if (pluginSearchText.isNotBlank()) {
val matchingPluginStatuses = reflectiveEditorPlugin.getPluginStatuses().filter { pluginStatus ->
!pluginStatus.isInstalled &&
!isKondoKit(pluginStatus.name) &&
(pluginStatus.name.contains(pluginSearchText, ignoreCase = true) ||
(pluginStatus.description?.contains(pluginSearchText, ignoreCase = true) ?: false))
}
if (matchingPluginStatuses.isNotEmpty()) {
val separator = JPanel().apply {
background = VIEW_BACKGROUND_COLOR
preferredSize = Dimension(Int.MAX_VALUE, 1)
maximumSize = preferredSize
}
contentPanel.add(Box.createVerticalStrut(10))
contentPanel.add(separator)
contentPanel.add(Box.createVerticalStrut(5))
val headerPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
background = VIEW_BACKGROUND_COLOR
add(JLabel("Available Plugins").apply {
foreground = secondaryColor
font = ViewConstants.FONT_RUNESCAPE_SMALL_BOLD_16
})
}
contentPanel.add(headerPanel)
contentPanel.add(Box.createVerticalStrut(5))
matchingPluginStatuses.forEach { pluginStatus ->
val pluginPanel = createPluginStatusItemPanel(pluginStatus)
contentPanel.add(pluginPanel)
contentPanel.add(Box.createVerticalStrut(5))
}
}
}
}
contentPanel.revalidate()
contentPanel.repaint()
}
private fun createPluginItemPanel(pluginInfo: PluginInfo, plugin: Plugin): JPanel {
val panel = createPluginWidgetPanel().apply {
layout = BorderLayout()
}
val packageName = plugin.javaClass.`package`.name
val nameLabel = JLabel(packageName)
nameLabel.foreground = secondaryColor
nameLabel.font = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16
val exposedFields = plugin.javaClass.declaredFields.filter { field ->
field.annotations.any { annotation ->
annotation.annotationClass.simpleName == "Exposed"
}
}
val editButton = exposedFields.takeIf { it.isNotEmpty() }?.let {
UiStyler.button(text = "Edit") {
showPluginDetails(pluginInfo, plugin)
}
}
val toggleSwitch = if (!isKondoKit(packageName)) {
createToggleSwitch(plugin, pluginInfo)
} else {
val placeholder = JPanel()
placeholder.background = WIDGET_COLOR
placeholder.setFixedSize(ViewConstants.TOGGLE_PLACEHOLDER_SIZE)
placeholder.isVisible = false
placeholder
}
val infoPanel = JPanel(BorderLayout())
infoPanel.background = WIDGET_COLOR
infoPanel.add(nameLabel, BorderLayout.WEST)
val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, -3))
controlsPanel.background = WIDGET_COLOR
editButton?.let { controlsPanel.add(it) }
if (!isKondoKit(packageName)) {
controlsPanel.add(toggleSwitch)
}
panel.add(infoPanel, BorderLayout.CENTER)
panel.add(controlsPanel, BorderLayout.EAST)
if (!isKondoKit(packageName)) {
val popupMenu = createPluginContextMenu(plugin, pluginInfo, packageName, false)
panel.attachPopupMenu(popupMenu)
}
return panel
}
private fun createDisabledPluginItemPanel(pluginName: String): JPanel {
val panel = createPluginWidgetPanel().apply {
layout = BorderLayout()
}
val nameLabel = JLabel(pluginName)
nameLabel.foreground = secondaryColor
nameLabel.font = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16
val toggleSwitch = ToggleSwitch()
toggleSwitch.setActivated(false)
toggleSwitch.onToggleListener = { activated ->
if (activated) {
reflectiveEditorPlugin.enablePlugin(pluginName, mainPanel ?: JPanel())
}
else {
toggleSwitch.setActivated(false)
}
}
val infoPanel = JPanel(BorderLayout())
infoPanel.background = WIDGET_COLOR
infoPanel.add(nameLabel, BorderLayout.WEST)
val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 0, 0))
controlsPanel.background = WIDGET_COLOR
controlsPanel.add(toggleSwitch)
panel.add(infoPanel, BorderLayout.CENTER)
panel.add(controlsPanel, BorderLayout.EAST)
if (!isKondoKit(pluginName)) {
val popupMenu = createPluginContextMenu(null, null, pluginName, true)
panel.attachPopupMenu(popupMenu)
}
return panel
}
private fun createPluginStatusItemPanel(pluginStatus: PluginInfoWithStatus): JPanel {
val panel = createPluginWidgetPanel().apply {
layout = BorderLayout()
}
val nameLabel = JLabel(pluginStatus.name)
nameLabel.foreground = secondaryColor
nameLabel.font = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16
val actionButton = UiStyler.button()
val progressBar = JProgressBar(0, 100)
progressBar.isStringPainted = true
progressBar.isVisible = false
progressBar.string = "Downloading..."
if (pluginStatus.isDownloading) {
actionButton.isVisible = false
progressBar.isVisible = true
progressBar.value = pluginStatus.downloadProgress
} else if (pluginStatus.isInstalled) {
if (pluginStatus.needsUpdate) {
actionButton.text = "Update"
actionButton.addActionListener {
startPluginDownload(pluginStatus)
}
} else {
actionButton.text = "Installed"
actionButton.isEnabled = false
}
} else {
actionButton.text = "Download"
actionButton.addActionListener {
startPluginDownload(pluginStatus)
}
}
val toggleSwitch = ToggleSwitch()
// Skip the toggle switch for KondoKit since it should always be enabled
if (isKondoKit(pluginStatus.name)) {
toggleSwitch.isVisible = false
} else {
toggleSwitch.setActivated(pluginStatus.isInstalled && !pluginStatus.needsUpdate)
toggleSwitch.isEnabled = pluginStatus.isInstalled && !pluginStatus.needsUpdate
if (pluginStatus.isInstalled && !pluginStatus.needsUpdate) {
toggleSwitch.onToggleListener = { activated ->
val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins")
loadedPluginsField.isAccessible = true
val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *>
var foundPlugin: Plugin? = null
var foundPluginInfo: PluginInfo? = null
for ((pluginInfo, plugin) in loadedPlugins) {
if (getPluginDirName(plugin as Plugin) == pluginStatus.name) {
foundPlugin = plugin
foundPluginInfo = pluginInfo as PluginInfo
break
}
}
if (foundPlugin != null && foundPluginInfo != null) {
reflectiveEditorPlugin.togglePlugin(foundPlugin, foundPluginInfo, toggleSwitch, activated, mainPanel ?: JPanel())
} else {
val pluginDirName = pluginStatus.name
val sourceDir = if (activated) {
File(File(reflectiveEditorPlugin.getPluginsDirectory(), "disabled"), pluginDirName)
} else {
File(reflectiveEditorPlugin.getPluginsDirectory(), pluginDirName)
}
val destDir = if (activated) {
File(reflectiveEditorPlugin.getPluginsDirectory(), pluginDirName)
} else {
val disabledDir = File(reflectiveEditorPlugin.getPluginsDirectory(), "disabled")
if (!disabledDir.exists()) {
disabledDir.mkdirs()
}
File(disabledDir, pluginDirName)
}
if (!sourceDir.exists()) {
showToast(mainPanel, "Plugin directory not found: ${sourceDir.absolutePath}", JOptionPane.ERROR_MESSAGE)
toggleSwitch.setActivated(!activated)
} else {
if (sourceDir.renameTo(destDir)) {
showToast(mainPanel, if (activated) "Plugin enabled" else "Plugin disabled")
reflectiveEditorPlugin.setReloadPlugins(true) // Schedule plugin reload to avoid crashes
SwingUtilities.invokeLater {
addPlugins(reflectiveEditorView!!)
}
} else {
showToast(mainPanel, "Failed to ${if (activated) "enable" else "disable"} plugin", JOptionPane.ERROR_MESSAGE)
toggleSwitch.setActivated(!activated) // Reset toggle to previous state on failure
}
}
}
}
} else {
toggleSwitch.isVisible = false
}
}
val infoPanel = JPanel(BorderLayout())
infoPanel.background = WIDGET_COLOR
infoPanel.add(nameLabel, BorderLayout.WEST)
val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 0, 0))
controlsPanel.background = WIDGET_COLOR
controlsPanel.add(actionButton)
controlsPanel.add(progressBar)
if (toggleSwitch.isVisible) {
controlsPanel.add(toggleSwitch)
}
if (!pluginStatus.isInstalled) {
val tooltipText = buildInstallableTooltip(pluginStatus)
panel.toolTipText = tooltipText
actionButton.toolTipText = tooltipText
progressBar.toolTipText = tooltipText
} else {
panel.toolTipText = null
actionButton.toolTipText = null
progressBar.toolTipText = null
}
panel.add(infoPanel, BorderLayout.CENTER)
panel.add(controlsPanel, BorderLayout.EAST)
if (!isKondoKit(pluginStatus.name) && pluginStatus.isInstalled) {
val popupMenu = createPluginContextMenu(null, null, pluginStatus.name, !pluginStatus.needsUpdate)
panel.attachPopupMenu(popupMenu)
}
return panel
}
private fun createPluginWidgetPanel(): WidgetPanel {
return WidgetPanel(
widgetWidth = ViewConstants.PLUGIN_LIST_ITEM_SIZE.width,
widgetHeight = ViewConstants.PLUGIN_LIST_ITEM_SIZE.height,
addDefaultPadding = false,
paddingTop = 10,
paddingLeft = 10,
paddingBottom = 10,
paddingRight = 10
)
}
private fun buildInstallableTooltip(pluginStatus: PluginInfoWithStatus): String {
val lines = mutableListOf<String>()
pluginStatus.remoteVersion?.takeIf { it.isNotBlank() }?.let {
lines += "<b>Version:</b> ${escapeHtml(it)}"
}
pluginStatus.author?.takeIf { it.isNotBlank() }?.let {
lines += "<b>Author:</b> ${escapeHtml(it)}"
}
pluginStatus.description?.takeIf { it.isNotBlank() }?.let {
lines += escapeHtml(it).replace("\n", "<br>")
}
if (lines.isEmpty()) {
lines += "Click Download to install this plugin."
}
return "<html>${lines.joinToString("<br>")}</html>"
}
private fun escapeHtml(value: String): String {
return value
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}
private fun createToggleSwitch(plugin: Plugin, pluginInfo: PluginInfo): ToggleSwitch {
val toggleSwitch = ToggleSwitch()
toggleSwitch.setActivated(reflectiveEditorPlugin.isPluginEnabled(plugin, pluginInfo))
toggleSwitch.onToggleListener = { activated ->
reflectiveEditorPlugin.togglePlugin(plugin, pluginInfo, toggleSwitch, activated, mainPanel ?: JPanel())
}
return toggleSwitch
}
private fun getPluginDirName(plugin: Plugin): String {
// The package name is typically like "GroundItems.plugin" so we take the first part
val packageName = plugin.javaClass.`package`.name
return packageName.substringBefore(".")
}
private fun showPluginDetails(pluginInfo: PluginInfo, plugin: Plugin) {
currentPluginInfo = pluginInfo
currentPlugin = plugin
searchFieldWrapper?.isVisible = false
val existingDetailView = mainPanel.components.find { it.name == PLUGIN_DETAIL_VIEW }
if (existingDetailView != null) {
mainPanel.remove(existingDetailView)
}
val detailView = BaseView(PLUGIN_DETAIL_VIEW)
detailView.layout = BorderLayout()
detailView.background = VIEW_BACKGROUND_COLOR
val packageName = plugin.javaClass.`package`.name
val headerPanel = ViewHeader("$packageName v${pluginInfo.version}", 40).apply {
border = BorderFactory.createEmptyBorder(5, 10, 5, 10)
}
val backButton = ButtonPanel(FlowLayout.LEFT).addButton("Back") {
SwingUtilities.invokeLater {
// Find the ScrollablePanel in the plugin list view and reset its scroll position
val listView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW }
if (listView != null && listView is Container) {
findAndResetScrollablePanel(listView)
}
}
searchFieldWrapper?.isVisible = true
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
}
headerPanel.add(backButton, BorderLayout.WEST)
val contentPanel = JPanel()
contentPanel.layout = BoxLayout(contentPanel, BoxLayout.Y_AXIS)
contentPanel.background = VIEW_BACKGROUND_COLOR
contentPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
val infoPanel = JPanel(BorderLayout())
infoPanel.background = WIDGET_COLOR
infoPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
val infoText = """
<html>
Version: ${(pluginInfo.version)}<br>
Author: ${(pluginInfo.author ?: "").trim('\'')}<br>
Description: ${(pluginInfo.description ?: "").trim('\'')}
</html>
""".trimIndent()
val infoFont = ViewConstants.FONT_RUNESCAPE_SMALL_14
val infoLabel = LabelComponent(infoText, isHtml = true).apply {
font = infoFont
}
infoPanel.add(infoLabel, BorderLayout.CENTER)
val fm = infoLabel.getFontMetrics(infoFont)
val avgCharWidth = fm.stringWidth("x")
val availableWidth = 200
val charsPerLine = availableWidth / avgCharWidth
val textContent = infoText.replace("<[^>]*>".toRegex(), "")
val lines = textContent.split('\n').filter { it.isNotBlank() }
var totalLines = 0
for (line in lines) {
val lineLength = line.length
totalLines += kotlin.math.ceil(lineLength.toDouble() / charsPerLine).toInt()
}
val lineHeight = fm.height
val estimatedHeight = (totalLines * lineHeight) + 20
val finalHeight = kotlin.math.max(60, kotlin.math.min(estimatedHeight, 150))
infoPanel.maximumSize = Dimension(Int.MAX_VALUE, finalHeight)
infoPanel.preferredSize = Dimension(ViewConstants.DEFAULT_WIDGET_SIZE.width, finalHeight)
contentPanel.add(infoPanel)
contentPanel.add(Box.createVerticalStrut(10))
addPluginSettings(contentPanel, plugin)
val scrollablePanel = ScrollablePanel(contentPanel)
SwingUtilities.invokeLater {
resetScrollablePanel(scrollablePanel)
}
detailView.add(headerPanel, BorderLayout.NORTH)
detailView.add(scrollablePanel, BorderLayout.CENTER)
mainPanel.add(detailView, PLUGIN_DETAIL_VIEW)
mainPanel.revalidate()
mainPanel.repaint()
cardLayout.show(mainPanel, PLUGIN_DETAIL_VIEW)
}
private fun addPluginSettings(contentPanel: JPanel, plugin: Plugin) {
val settingsPanel = SettingsPanel(plugin)
settingsPanel.addSettingsFromPlugin()
contentPanel.add(settingsPanel)
}
fun addPlugins(reflectiveEditorView: JPanel) {
// Ensure we run on the EDT; if not, reschedule and return
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater { addPlugins(reflectiveEditorView) }
return
}
// Check if we need to reload plugins
if (reflectiveEditorPlugin.shouldReloadPlugins()) {
PluginRepository.reloadPlugins()
reflectiveEditorPlugin.setReloadPlugins(false)
}
// Update plugin statuses to reflect current loaded plugins
reflectiveEditorPlugin.updatePluginStatuses()
val contentPanel = pluginListContentPanel
if (contentPanel == null) {
// Fallback path: rebuild the list view if it was not initialized yet
val existingListView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW }
val pluginListView = createPluginListView()
pluginListView.name = PLUGIN_LIST_VIEW
if (existingListView != null) {
mainPanel.remove(existingListView)
}
mainPanel.add(pluginListView, PLUGIN_LIST_VIEW)
} else {
contentPanel.isVisible = false
try {
populatePluginListContent()
} finally {
contentPanel.isVisible = true
}
}
searchFieldWrapper?.isVisible = true
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
pluginListScrollablePanel?.let { scrollPanel ->
SwingUtilities.invokeLater {
resetScrollablePanel(scrollPanel)
}
}
mainPanel.revalidate()
mainPanel.repaint()
}
private fun findAndResetScrollablePanel(container: Component) {
if (container is ScrollablePanel) {
resetScrollablePanel(container)
} else if (container is Container) {
for (child in container.components) {
findAndResetScrollablePanel(child)
}
}
}
private fun startPluginDownload(pluginStatus: PluginInfoWithStatus) {
reflectiveEditorPlugin.startPluginDownload(pluginStatus, mainPanel ?: JPanel())
}
private fun createPluginContextMenu(plugin: Plugin?, pluginInfo: PluginInfo?, pluginName: String, isDisabled: Boolean): JPopupMenu {
val popupMenu = PopupMenuComponent()
popupMenu.addMenuItem("Delete Plugin") {
deletePlugin(pluginName, isDisabled)
}
return popupMenu
}
private fun deletePlugin(pluginName: String, isDisabled: Boolean) {
reflectiveEditorPlugin.deletePlugin(pluginName, isDisabled, mainPanel ?: JPanel())
}
private fun resetScrollablePanel(scrollablePanel: ScrollablePanel) {
try {
val contentField = ScrollablePanel::class.java.getDeclaredField("content")
contentField.isAccessible = true
val contentPanel = contentField.get(scrollablePanel) as JPanel
// Reset the location to the top
contentPanel.setLocation(0, 0)
// Force a repaint
scrollablePanel.revalidate()
scrollablePanel.repaint()
} catch (e: Exception) {
// If reflection fails, at least try to repaint
scrollablePanel.revalidate()
scrollablePanel.repaint()
}
}
}

View file

@ -0,0 +1,72 @@
package KondoKit.views
import KondoKit.ui.ViewConstants
import KondoKit.ui.components.SearchField
import KondoKit.ui.components.WidgetPanel
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import java.awt.Color
import java.awt.Component
import java.awt.Container
import java.awt.Dimension
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JPanel
object ViewLayoutHelpers {
data class SearchFieldSection(
val wrapper: JPanel,
val searchField: SearchField
)
/**
* Creates a standard search field row with consistent sizing and styling.
*/
fun createSearchFieldSection(
parent: JPanel,
placeholderText: String,
viewName: String,
initialText: String = "",
fieldWidth: Int = ViewConstants.SEARCH_FIELD_SIZE.width,
fieldHeight: Int = ViewConstants.SEARCH_FIELD_SIZE.height,
onSearch: (String) -> Unit
): SearchFieldSection {
val searchField = SearchField(
parentPanel = parent,
onSearch = onSearch,
placeholderText = placeholderText,
fieldWidth = fieldWidth,
fieldHeight = fieldHeight,
viewName = viewName
)
if (initialText.isNotBlank()) {
searchField.setText(initialText)
}
val preferredSize = Dimension(fieldWidth, fieldHeight)
val wrapper = WidgetPanel(
widgetWidth = preferredSize.width,
widgetHeight = preferredSize.height,
addDefaultPadding = false
).apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
background = VIEW_BACKGROUND_COLOR
alignmentX = Component.CENTER_ALIGNMENT
add(searchField)
}
return SearchFieldSection(wrapper = wrapper, searchField = searchField)
}
fun createVerticalPanel(background: Color = VIEW_BACKGROUND_COLOR): JPanel {
return JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
this.background = background
}
}
}
fun Container.addVerticalSpacing(pixels: Int) {
add(Box.createVerticalStrut(pixels))
}

View file

@ -0,0 +1,459 @@
package KondoKit.views
import KondoKit.util.Helpers
import KondoKit.util.Helpers.formatHtmlLabelText
import KondoKit.util.Helpers.formatNumber
import KondoKit.util.Helpers.getProgressBarColor
import KondoKit.util.Helpers.getSpriteId
import KondoKit.util.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.ui.BaseView
import KondoKit.ui.OnUpdateCallback
import KondoKit.ui.OnXPUpdateCallback
import KondoKit.ui.ViewConstants
import KondoKit.ui.View
import KondoKit.util.setFixedSize
import KondoKit.util.attachPopupMenu
import KondoKit.util.XPTable
import KondoKit.ui.components.PopupMenuComponent
import KondoKit.ui.components.ProgressBar
import KondoKit.ui.components.LabelComponent
import KondoKit.ui.components.WidgetPanel
import KondoKit.ui.components.XPWidget
import KondoKit.plugin.Companion.IMAGE_SIZE
import KondoKit.plugin.Companion.LVL_ICON
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin
import KondoKit.plugin.Companion.playerXPMultiplier
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import KondoKit.util.JsonParser
import plugin.api.API
import java.awt.*
import java.awt.image.BufferedImage
import javax.swing.*
import javax.swing.SwingConstants
object XPTrackerView : View, OnUpdateCallback, OnXPUpdateCallback {
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4)
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
var totalXPWidget: XPWidget? = null
val initialXP: MutableMap<Int, Int> = HashMap()
var xpTrackerView: JPanel? = null
const val VIEW_NAME = "XP_TRACKER_VIEW"
override val name: String = VIEW_NAME
override val iconSpriteId: Int = LVL_ICON
private val skillIconCache: MutableMap<Int, BufferedImage> = HashMap()
val npcHitpointsMap: Map<Int, Int> = try {
val json = Helpers.readResourceText("res/npc_hitpoints_map.json") ?: "{}"
val parsed: Map<String, Int> = JsonParser.fromJson(json)
parsed.mapNotNull { (key, value) ->
key.toIntOrNull()?.let { it to value }
}.toMap()
} catch (e: Exception) {
println("XPTracker Error parsing NPC HP: ${e.message}")
emptyMap()
}
private val widgetFont = ViewConstants.FONT_RUNESCAPE_SMALL_16
private fun BufferedImage.ensureOpaque(): BufferedImage {
for (y in 0 until height) {
for (x in 0 until width) {
val color = getRGB(x, y)
if (color != 0) {
setRGB(x, y, color or (0xFF shl 24))
}
}
}
return this
}
private fun createIconContainer(image: BufferedImage): JPanel {
val processed = image.ensureOpaque()
val icon = ImageIcon(processed)
val label = JLabel(icon).apply {
horizontalAlignment = SwingConstants.CENTER
verticalAlignment = SwingConstants.CENTER
isOpaque = false
}
return JPanel(BorderLayout()).apply {
background = WIDGET_COLOR
val wrapperSize = Dimension(IMAGE_SIZE)
preferredSize = wrapperSize
minimumSize = wrapperSize
maximumSize = wrapperSize
add(label, BorderLayout.CENTER)
}
}
private fun createMetricLabel(title: String, initialValue: String = "0", topPadding: Int = 0): LabelComponent =
LabelComponent(alignment = JLabel.LEFT).apply {
updateHtmlText("$title ", primaryColor, initialValue, secondaryColor)
if (topPadding > 0) {
border = BorderFactory.createEmptyBorder(topPadding, 0, 0, 0)
}
}
private fun getSkillIcon(skillId: Int): BufferedImage {
return skillIconCache[skillId] ?: getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId))).also {
skillIconCache[skillId] = it
}
}
private fun createTotalWidgetContainer(popupMenu: JPopupMenu): Container {
totalXPWidget = createTotalXPWidget()
return wrappedWidget(totalXPWidget!!.container, padding = 0).also {
it.attachPopupMenu(popupMenu, includeChildren = true)
}
}
override val panel: JPanel
get() = xpTrackerView ?: JPanel()
override fun createView() {
createXPTrackerView()
}
override fun registerFunctions() {
plugin.registerUpdateCallback(this)
plugin.registerXPUpdateCallback(this)
}
override fun onUpdate() {
val widgets = xpWidgets.values
val totalXP = totalXPWidget
widgets.forEach { xpWidget ->
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedXpPerHour = formatNumber(xpPerHour)
xpWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
xpWidget.container.repaint()
}
totalXP?.let { widget ->
val elapsedTime = (System.currentTimeMillis() - widget.startTime) / 1000.0 / 60.0 / 60.0
val totalXPPerHour = if (elapsedTime > 0) (widget.totalXpGained / elapsedTime).toInt() else 0
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
widget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
widget.container.repaint()
}
}
override fun onXPUpdate(skillId: Int, xp: Int) {
if (!initialXP.containsKey(skillId)) {
initialXP[skillId] = xp
return
}
val previousXpSnapshot = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
val ensureOnEdt = Runnable {
var xpWidget = xpWidgets[skillId]
if (xpWidget != null) {
updateWidget(xpWidget, xp)
} else {
xpWidget = createXPWidget(skillId, previousXpSnapshot)
xpWidgets[skillId] = xpWidget
val wrapped = wrappedWidget(xpWidget.container, padding = 0, innerPadding = 6)
val popupMenu = removeXPWidgetMenu(wrapped, skillId)
wrapped.attachPopupMenu(popupMenu, includeChildren = true)
xpTrackerView?.add(wrapped)
xpTrackerView?.add(Box.createVerticalStrut(5))
if(focusedView == VIEW_NAME) {
xpTrackerView?.revalidate()
xpTrackerView?.repaint()
}
updateWidget(xpWidget, xp)
}
}
if (SwingUtilities.isEventDispatchThread()) {
ensureOnEdt.run()
} else {
SwingUtilities.invokeLater(ensureOnEdt)
}
}
fun updateWidget(xpWidget: XPWidget, xp: Int) {
val (currentLevel, xpGainedSinceLastLevel) = XPTable.getLevelForXp(xp)
var xpGainedSinceLastUpdate = xp - xpWidget.previousXp
xpWidget.totalXpGained += xpGainedSinceLastUpdate
updateTotalXPWidget(xpGainedSinceLastUpdate)
val progress: Double
if (currentLevel >= 99) {
progress = 100.0 // Set progress to 100% if the level is 99 or above
xpWidget.xpLeftLabel.text = "" // Hide XP Left when level is 99
xpWidget.actionsRemainingLabel.text = ""
} else {
val nextLevelXp = XPTable.getXpRequiredForLevel(currentLevel + 1)
val xpLeft = nextLevelXp - xp
progress = xpGainedSinceLastLevel.toDouble() / (nextLevelXp - XPTable.getXpRequiredForLevel(currentLevel)) * 100
val xpLeftstr = formatNumber(xpLeft)
xpWidget.xpLeftLabel.text = formatHtmlLabelText("XP Left: ", primaryColor, xpLeftstr, secondaryColor)
if(COMBAT_SKILLS.contains(xpWidget.skillId)) {
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
val xpPerKill = when (xpWidget.skillId) {
3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints
else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
}
val remainingKills = xpLeft / xpPerKill
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
}
} else {
if(xpGainedSinceLastUpdate == 0)
xpGainedSinceLastUpdate = 1 // Avoid possible divide by 0
val remainingActions = (xpLeft / xpGainedSinceLastUpdate).coerceAtLeast(1)
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Actions: ", primaryColor, remainingActions.toString(), secondaryColor)
}
}
val formattedXp = formatNumber(xpWidget.totalXpGained)
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME)
xpWidget.previousXp = xp
if (focusedView == VIEW_NAME)
xpWidget.container.repaint()
}
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
val totalXPWidget = totalXPWidget ?: return
totalXPWidget.totalXpGained += xpGainedSinceLastUpdate
val formattedXp = formatNumber(totalXPWidget.totalXpGained)
totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor)
if (focusedView == VIEW_NAME)
totalXPWidget.container.repaint()
}
fun resetXPTracker(xpTrackerView: JPanel) {
xpTrackerView.removeAll()
val popupMenu = createResetMenu()
xpTrackerView.add(Box.createVerticalStrut(5))
xpTrackerView.add(createTotalWidgetContainer(popupMenu))
xpTrackerView.add(Box.createVerticalStrut(5))
initialXP.clear()
xpWidgets.clear()
xpTrackerView.revalidate()
if (focusedView == VIEW_NAME) {
xpTrackerView.repaint()
}
}
fun createTotalXPWidget(): XPWidget {
val widgetPanel = WidgetPanel(
widgetWidth = ViewConstants.DEFAULT_WIDGET_SIZE.width,
widgetHeight = ViewConstants.TOTAL_XP_WIDGET_SIZE.height,
addDefaultPadding = false,
paddingTop = 10,
paddingBottom = 10,
paddingRight = 10,
paddingLeft = 10
)
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(LVL_ICON))
val (_, imageContainer) = Helpers.createImageCanvasComponents(
bufferedImageSprite,
borderInsets = Insets(0, 0, 0, 5)
)
val textPanel = JPanel(GridLayout(2, 1, 5, 0)).apply {
background = WIDGET_COLOR
}
val xpGainedLabel = createMetricLabel("Gained:")
val xpPerHourLabel = createMetricLabel("XP /hr:")
textPanel.add(xpGainedLabel)
textPanel.add(xpPerHourLabel)
widgetPanel.setFixedSize(
ViewConstants.DEFAULT_WIDGET_SIZE.width,
ViewConstants.TOTAL_XP_WIDGET_SIZE.height
)
widgetPanel.add(imageContainer, BorderLayout.WEST)
widgetPanel.add(textPanel, BorderLayout.CENTER)
return XPWidget(
skillId = -1,
container = widgetPanel,
xpGainedLabel = xpGainedLabel,
xpLeftLabel = createMetricLabel("XP Left:"),
xpPerHourLabel = xpPerHourLabel,
progressBar = ProgressBar(0.0, Color.BLACK), // Unused
totalXpGained = 0,
startTime = System.currentTimeMillis(),
previousXp = 0,
actionsRemainingLabel = JLabel().apply { font = widgetFont },
)
}
fun createXPTrackerView() {
val widgetViewPanel = BaseView(VIEW_NAME, addDefaultSpacing = false).apply {
add(Box.createVerticalStrut(5))
}
val popupMenu = createResetMenu()
widgetViewPanel.add(createTotalWidgetContainer(popupMenu))
widgetViewPanel.add(Box.createVerticalStrut(5))
xpTrackerView = widgetViewPanel
// Preload skill icons to avoid first-drop lag
try {
for (i in 0 until 24) {
getSkillIcon(i)
}
} catch (_: Exception) { }
}
fun createResetMenu(): JPopupMenu {
val popupMenu = PopupMenuComponent()
popupMenu.addMenuItem("Reset Tracker") {
plugin.registerDrawAction { resetXPTracker(xpTrackerView!!) }
}
return popupMenu
}
fun removeXPWidgetMenu(toRemove: Container, skillId: Int): JPopupMenu {
val popupMenu = PopupMenuComponent()
popupMenu.addMenuItem("Reset") {
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)
}
}
popupMenu.addMenuItem("Remove") {
// 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 = WidgetPanel(
widgetWidth = ViewConstants.DEFAULT_WIDGET_SIZE.width,
widgetHeight = 56,
addDefaultPadding = false
)
val iconContainer = createIconContainer(getSkillIcon(skillId))
val textPanel = JPanel(GridLayout(2, 2, 5, 0)).apply {
background = WIDGET_COLOR
}
val xpGainedLabel = createMetricLabel("XP Gained:")
val xpLeftLabel = createMetricLabel("XP Left:", "0K")
val xpPerHourLabel = createMetricLabel("XP /hr:")
val actionsTitle = if (COMBAT_SKILLS.contains(skillId)) "Kills:" else "Actions:"
val actionsLabel = createMetricLabel(actionsTitle)
val progressBar = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
setFixedSize(160, 22)
}
val progressPanel = JPanel(BorderLayout()).apply {
background = WIDGET_COLOR
add(progressBar, BorderLayout.CENTER)
}
textPanel.add(xpGainedLabel)
textPanel.add(xpLeftLabel)
textPanel.add(xpPerHourLabel)
textPanel.add(actionsLabel)
widgetPanel.add(iconContainer, BorderLayout.WEST)
widgetPanel.add(textPanel, BorderLayout.CENTER)
widgetPanel.add(progressPanel, BorderLayout.SOUTH)
widgetPanel.revalidate()
if(focusedView == VIEW_NAME)
widgetPanel.repaint()
return XPWidget(
skillId = skillId,
container = widgetPanel,
xpGainedLabel = xpGainedLabel,
xpLeftLabel = xpLeftLabel,
xpPerHourLabel = xpPerHourLabel,
progressBar = progressBar,
totalXpGained = 0,
actionsRemainingLabel = actionsLabel,
startTime = System.currentTimeMillis(),
previousXp = previousXp
)
}
fun wrappedWidget(component: Component, padding: Int = 7, innerPadding: Int = 0): Container {
val outerPanelSize = Dimension(
component.preferredSize.width + 2 * padding,
component.preferredSize.height + 2 * padding
)
val outerPanel = JPanel(GridBagLayout()).apply {
background = WIDGET_COLOR
setFixedSize(outerPanelSize)
}
val innerPanel = JPanel(BorderLayout()).apply {
background = WIDGET_COLOR
setFixedSize(component.preferredSize)
if (innerPadding > 0) {
border = BorderFactory.createEmptyBorder(innerPadding, innerPadding, innerPadding, innerPadding)
}
add(component, BorderLayout.CENTER)
}
val gbc = GridBagConstraints().apply {
anchor = GridBagConstraints.CENTER
}
outerPanel.add(innerPanel, gbc)
return outerPanel
}
}