Merge branch 'KondoKit-2.0' into 'master'

KondoKit 2.0

See merge request 2009scape/rt4-client!28
This commit is contained in:
downthecrop 2024-10-28 23:43:44 +00:00
commit 7594e98f51
36 changed files with 2493 additions and 869 deletions

View file

@ -13,6 +13,7 @@ import rt4.Tile;
*/ */
public abstract class Plugin { public abstract class Plugin {
long timeOfLastDraw; long timeOfLastDraw;
long timeOfLastLateDraw;
void _init() { void _init() {
Init(); Init();
@ -24,6 +25,12 @@ public abstract class Plugin {
timeOfLastDraw = nowTime; timeOfLastDraw = nowTime;
} }
void _lateDraw() {
long nowTime = System.currentTimeMillis();
LateDraw(nowTime - timeOfLastLateDraw);
timeOfLastLateDraw = nowTime;
}
/** /**
* Draw() is called by the client rendering loop so that plugins can draw information onto the screen. * Draw() is called by the client rendering loop so that plugins can draw information onto the screen.
* This will be called once per frame, meaning it is framerate bound. * This will be called once per frame, meaning it is framerate bound.
@ -31,6 +38,14 @@ public abstract class Plugin {
*/ */
public void Draw(long timeDelta) {} public void Draw(long timeDelta) {}
/**
* LateDraw() is called at the end of a finalized frame
* This will be called once per frame, meaning it is framerate bound.
* @param timeDelta the time (ms) elapsed since the last draw call.
*/
public void LateDraw(long timeDelta) {}
/** /**
* Init() is called when the plugin is first loaded * Init() is called when the plugin is first loaded
*/ */

View file

@ -13,10 +13,10 @@ import java.util.Properties;
* A data class for storing information about plugins. * A data class for storing information about plugins.
* @author ceikry * @author ceikry
*/ */
class PluginInfo { public class PluginInfo {
double version; public double version;
String author; public String author;
String description; public String description;
public PluginInfo(String author, String description, double version) { public PluginInfo(String author, String description, double version) {
this.version = version; this.version = version;

View file

@ -160,6 +160,11 @@ public class PluginRepository {
pluginsSnapshot.forEach(Plugin::_draw); pluginsSnapshot.forEach(Plugin::_draw);
} }
public static void LateDraw() {
List<Plugin> pluginsSnapshot = new ArrayList<>(loadedPlugins.values());
pluginsSnapshot.forEach(Plugin::_lateDraw);
}
public static void NPCOverheadDraw(Npc npc, int screenX, int screenY) { public static void NPCOverheadDraw(Npc npc, int screenX, int screenY) {
loadedPlugins.values().forEach((plugin) -> plugin.NPCOverheadDraw(npc, screenX, screenY)); loadedPlugins.values().forEach((plugin) -> plugin.NPCOverheadDraw(npc, screenX, screenY));
} }

View file

@ -12,7 +12,7 @@ import java.awt.*;
public abstract class FrameBuffer { public abstract class FrameBuffer {
@OriginalMember(owner = "client!vk", name = "e", descriptor = "[I") @OriginalMember(owner = "client!vk", name = "e", descriptor = "[I")
protected int[] pixels; public int[] pixels;
@OriginalMember(owner = "client!vk", name = "g", descriptor = "Ljava/awt/Image;") @OriginalMember(owner = "client!vk", name = "g", descriptor = "Ljava/awt/Image;")
protected Image image; protected Image image;

View file

@ -9,6 +9,7 @@ import org.openrs2.deob.annotation.OriginalMember;
import org.openrs2.deob.annotation.Pc; import org.openrs2.deob.annotation.Pc;
import java.awt.*; import java.awt.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -24,6 +25,11 @@ public final class GlRenderer {
public static float hFOV = 0; public static float hFOV = 0;
private static ByteBuffer pixelByteBuffer;
private static IntBuffer pixelIntBuffer;
public static int[] pixelData;
@OriginalMember(owner = "client!tf", name = "c", descriptor = "F") @OriginalMember(owner = "client!tf", name = "c", descriptor = "F")
private static float aFloat30; private static float aFloat30;
@ -200,10 +206,34 @@ public final class GlRenderer {
public static void swapBuffers() { public static void swapBuffers() {
try { try {
drawable.swapBuffers(); drawable.swapBuffers();
readPixels();
} catch (@Pc(3) Exception local3) { } catch (@Pc(3) Exception local3) {
} }
} }
public static void initializePixelBuffer(int width, int height) {
// Allocate ByteBuffer for BGRA pixels (4 bytes per pixel)
pixelByteBuffer = ByteBuffer.allocateDirect(width * height * 4).order(ByteOrder.nativeOrder());
pixelIntBuffer = pixelByteBuffer.asIntBuffer();
pixelData = new int[width * height];
}
public static void readPixels() {
// Ensure the pixel buffer is initialized with the correct size
if (pixelByteBuffer == null || pixelIntBuffer.capacity() != canvasWidth * canvasHeight) {
initializePixelBuffer(canvasWidth, canvasHeight);
}
// Read pixels into the direct ByteBuffer
gl.glReadPixels(0, 0, canvasWidth, canvasHeight, GL2.GL_BGRA,
GlRenderer.bigEndian ? GL2.GL_UNSIGNED_INT_8_8_8_8_REV : GL2.GL_UNSIGNED_BYTE,
pixelByteBuffer);
// Convert to int array if needed
pixelIntBuffer.rewind(); // Prepare the IntBuffer for reading
pixelIntBuffer.get(pixelData, 0, pixelData.length); // Transfer to pixelData array if necessary
}
@OriginalMember(owner = "client!tf", name = "a", descriptor = "(Z)V") @OriginalMember(owner = "client!tf", name = "a", descriptor = "(Z)V")
public static void setFogEnabled(@OriginalArg(0) boolean enabled) { public static void setFogEnabled(@OriginalArg(0) boolean enabled) {
if (enabled == fogEnabled) { if (enabled == fogEnabled) {

View file

@ -921,6 +921,7 @@ public final class client extends GameShell {
Preferences.safeMode = false; Preferences.safeMode = false;
Preferences.write(GameShell.signLink); Preferences.write(GameShell.signLink);
} }
PluginRepository.LateDraw();
} }
@OriginalMember(owner = "client!client", name = "c", descriptor = "(B)V") @OriginalMember(owner = "client!client", name = "c", descriptor = "(B)V")

View file

@ -110,4 +110,10 @@ task buildPlugins(type: Copy, dependsOn: classes) {
from "build/classes/kotlin/main" from "build/classes/kotlin/main"
into pluginsPath into pluginsPath
} }
// Find and copy any 'res' directories from 'src/main/kotlin/**/res/'
copy {
from fileTree(dir: "src/main/kotlin", include: "**/res/**")
into pluginsPath
}
} }

View file

@ -1,7 +1,7 @@
package GroundItems package GroundItems
import KondoKit.Exposed
import plugin.Plugin import plugin.Plugin
import plugin.annotations.PluginMeta
import plugin.api.API.* import plugin.api.API.*
import plugin.api.FontColor.fromColor import plugin.api.FontColor.fromColor
import plugin.api.FontType import plugin.api.FontType
@ -18,27 +18,26 @@ import java.nio.charset.StandardCharsets
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.roundToInt import kotlin.math.roundToInt
@PluginMeta( class plugin : Plugin() {
author = "downthecrop",
description = @Exposed(description = "Default: true, Use Local JSON or the prices from the Live/Stable server API")
""" private var useLiveGEPrices = true
Ground Items Overlay. Just like Runelite! @Exposed( "Default: 5,000 (blue)")
cmds ::set(low,med,high,insane,hide), ::(tag,ignore)item ID, ::(reset)groundconfig private var lowValue = 5000
Special thanks to Chisato for the original skeleton. @Exposed( "Default: 20,000 (green)")
""", private var mediumValue = 20000
version = 1.2 @Exposed( "Default: 50,000 (orange)")
) private var highValue = 50000
open class plugin : Plugin() { @Exposed( "Default: 100,000 (pink)")
private var insaneValue = 100000
@Exposed("Default: 0, No labels will be drawn for items below this value (unless tagged)")
private var hideBelowValue = 0
@Exposed("Tag Items (purple) add/remove with Ctrl+RightClick Tag/Ignore")
private lateinit var taggedItems: List<Int>
@Exposed("Ignore items add/remove with Ctrl+RightClick Tag/Ignore")
private lateinit var ignoredItems: List<Int>
private var kondoExposed_lowValue = 5000
private var kondoExposed_mediumValue = 20000
private var kondoExposed_highValue = 50000
private var kondoExposed_insaneValue = 100000
private var kondoExposed_hideBelowValue = 0
private var kondoExposed_useLiveGEPrices = true
private val coindId = 995 private val coindId = 995
private lateinit var kondoExposed_taggedItems: List<Int>
private lateinit var kondoExposed_ignoredItems: List<Int>
private var gePriceMap = loadGEPrices() private var gePriceMap = loadGEPrices()
@ -60,23 +59,23 @@ open class plugin : Plugin() {
) )
override fun Init() { override fun Init() {
kondoExposed_lowValue = GetData("low-value") as? Int ?: 5000 lowValue = GetData("low-value") as? Int ?: 5000
kondoExposed_mediumValue = GetData("medium-value") as? Int ?: 20000 mediumValue = GetData("medium-value") as? Int ?: 20000
kondoExposed_highValue = GetData("high-value") as? Int ?: 50000 highValue = GetData("high-value") as? Int ?: 50000
kondoExposed_insaneValue = GetData("insane-value") as? Int ?: 100000 insaneValue = GetData("insane-value") as? Int ?: 100000
kondoExposed_hideBelowValue = GetData("hide-below-value") as? Int ?: 0 hideBelowValue = GetData("hide-below-value") as? Int ?: 0
kondoExposed_useLiveGEPrices = GetData("ground-item-use-remote") as? Boolean ?: true useLiveGEPrices = GetData("ground-item-use-remote") as? Boolean ?: true
kondoExposed_taggedItems = GetData("ground-item-tags")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList() taggedItems = GetData("ground-item-tags")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList()
kondoExposed_ignoredItems = GetData("ground-item-ignore")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList() ignoredItems = GetData("ground-item-ignore")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList()
if (gePriceMap.isEmpty()) SendMessage("Ground Items unable to load GE Prices, Remote: $kondoExposed_useLiveGEPrices") if (gePriceMap.isEmpty()) SendMessage("Ground Items unable to load GE Prices, Remote: $useLiveGEPrices")
} }
private fun isTagged(itemId: Int): Boolean { private fun isTagged(itemId: Int): Boolean {
return kondoExposed_taggedItems.contains(itemId) return taggedItems.contains(itemId)
} }
private fun isHidden(itemId: Int): Boolean { private fun isHidden(itemId: Int): Boolean {
return kondoExposed_ignoredItems.contains(itemId) return ignoredItems.contains(itemId)
} }
override fun Draw(timeDelta: Long) = renderGroundItemNames() override fun Draw(timeDelta: Long) = renderGroundItemNames()
@ -141,10 +140,10 @@ open class plugin : Plugin() {
val screenY = screenPos[1] val screenY = screenPos[1]
val color = when { val color = when {
isTagged(itemDef.id) -> colorMap["tagged"] isTagged(itemDef.id) -> colorMap["tagged"]
highestValue < kondoExposed_lowValue -> "#FFFFFF" highestValue < lowValue -> "#FFFFFF"
highestValue < kondoExposed_mediumValue -> colorMap["lowValue"] highestValue < mediumValue -> colorMap["lowValue"]
highestValue < kondoExposed_highValue -> colorMap["mediumValue"] highestValue < highValue -> colorMap["mediumValue"]
highestValue < kondoExposed_insaneValue -> colorMap["highValue"] highestValue < insaneValue -> colorMap["highValue"]
else -> colorMap["insaneValue"] else -> colorMap["insaneValue"]
} ?: "#FFFFFF" } ?: "#FFFFFF"
val colorInt = color.drop(1).toInt(16) val colorInt = color.drop(1).toInt(16)
@ -183,7 +182,7 @@ open class plugin : Plugin() {
val haValue = if (itemDef.id == coindId) item.value.amount else (itemDef.cost * 0.6 * item.value.amount).roundToInt() val haValue = if (itemDef.id == coindId) item.value.amount else (itemDef.cost * 0.6 * item.value.amount).roundToInt()
val geValue = (gePriceMap[itemDef.id.toString()]?.toInt() ?: 0) * item.value.amount val geValue = (gePriceMap[itemDef.id.toString()]?.toInt() ?: 0) * item.value.amount
val highestValue = maxOf(haValue, geValue) val highestValue = maxOf(haValue, geValue)
return !((highestValue < kondoExposed_hideBelowValue || isHidden(itemDef.id)) && !isTagged(itemDef.id)) return !((highestValue < hideBelowValue || isHidden(itemDef.id)) && !isTagged(itemDef.id))
} }
override fun OnMiniMenuCreate(currentEntries: Array<out MiniMenuEntry>?) { override fun OnMiniMenuCreate(currentEntries: Array<out MiniMenuEntry>?) {
@ -200,7 +199,7 @@ open class plugin : Plugin() {
private fun ignoreItem(itemId: Int): Runnable { private fun ignoreItem(itemId: Int): Runnable {
return Runnable { return Runnable {
val existingIgnores = kondoExposed_ignoredItems.toMutableList() val existingIgnores = ignoredItems.toMutableList()
if (existingIgnores.contains(itemId)) { if (existingIgnores.contains(itemId)) {
existingIgnores.remove(itemId) existingIgnores.remove(itemId)
@ -215,7 +214,7 @@ open class plugin : Plugin() {
private fun tagItem(itemId: Int): Runnable { private fun tagItem(itemId: Int): Runnable {
return Runnable { return Runnable {
val existingTags = kondoExposed_taggedItems.toMutableList() val existingTags = taggedItems.toMutableList()
if (existingTags.contains(itemId)) { if (existingTags.contains(itemId)) {
existingTags.remove(itemId) existingTags.remove(itemId)
@ -230,28 +229,28 @@ open class plugin : Plugin() {
private fun resetConfig() { private fun resetConfig() {
kondoExposed_lowValue = 5000 lowValue = 5000
kondoExposed_mediumValue = 20000 mediumValue = 20000
kondoExposed_highValue = 50000 highValue = 50000
kondoExposed_insaneValue = 100000 insaneValue = 100000
kondoExposed_hideBelowValue = 0 hideBelowValue = 0
kondoExposed_useLiveGEPrices = true useLiveGEPrices = true
StoreData("ground-item-tags",""); StoreData("ground-item-tags","");
StoreData("ground-item-ignore",""); StoreData("ground-item-ignore","");
StoreData("low-value", kondoExposed_lowValue) StoreData("low-value", lowValue)
StoreData("ground-item-use-remote", kondoExposed_useLiveGEPrices) StoreData("ground-item-use-remote", useLiveGEPrices)
StoreData("medium-value", kondoExposed_mediumValue) StoreData("medium-value", mediumValue)
StoreData("high-value", kondoExposed_highValue) StoreData("high-value", highValue)
StoreData("insane-value", kondoExposed_insaneValue) StoreData("insane-value", insaneValue)
StoreData("hide-below-value", kondoExposed_hideBelowValue) StoreData("hide-below-value", hideBelowValue)
} }
private fun displayRanges() { private fun displayRanges() {
val low = kondoExposed_lowValue val low = lowValue
val medium = kondoExposed_mediumValue val medium = mediumValue
val high = kondoExposed_highValue val high = highValue
val insane = kondoExposed_insaneValue val insane = insaneValue
val hide = kondoExposed_hideBelowValue val hide = hideBelowValue
SendMessage("== Ground Item Config ==") SendMessage("== Ground Item Config ==")
SendMessage("Low: $low") SendMessage("Low: $low")
@ -260,12 +259,12 @@ open class plugin : Plugin() {
SendMessage("Insane: $insane") SendMessage("Insane: $insane")
SendMessage("Hide Below: $hide") SendMessage("Hide Below: $hide")
SendMessage("-- Ignored Items --") SendMessage("-- Ignored Items --")
for(item in kondoExposed_ignoredItems){ for(item in ignoredItems){
val itemDef = ObjTypeList.get(item) val itemDef = ObjTypeList.get(item)
SendMessage("Ignored: ${itemDef.name} ${itemDef.id}") SendMessage("Ignored: ${itemDef.name} ${itemDef.id}")
} }
SendMessage("-- Tagged Items --") SendMessage("-- Tagged Items --")
for(item in kondoExposed_taggedItems){ for(item in taggedItems){
val itemDef = ObjTypeList.get(item) val itemDef = ObjTypeList.get(item)
SendMessage("Tagged: ${itemDef.name} ${itemDef.id}") SendMessage("Tagged: ${itemDef.name} ${itemDef.id}")
} }
@ -278,19 +277,19 @@ open class plugin : Plugin() {
} }
fun OnKondoValueUpdated() { fun OnKondoValueUpdated() {
StoreData("ground-item-tags",kondoExposed_taggedItems); StoreData("ground-item-tags",taggedItems);
StoreData("ground-item-ignore",kondoExposed_ignoredItems); StoreData("ground-item-ignore",ignoredItems);
StoreData("low-value", kondoExposed_lowValue) StoreData("low-value", lowValue)
StoreData("medium-value", kondoExposed_mediumValue) StoreData("medium-value", mediumValue)
StoreData("high-value", kondoExposed_highValue) StoreData("high-value", highValue)
StoreData("insane-value", kondoExposed_insaneValue) StoreData("insane-value", insaneValue)
StoreData("ground-item-use-remote", kondoExposed_useLiveGEPrices) StoreData("ground-item-use-remote", useLiveGEPrices)
StoreData("hide-below-value", kondoExposed_hideBelowValue) StoreData("hide-below-value", hideBelowValue)
gePriceMap = loadGEPrices(); gePriceMap = loadGEPrices();
} }
fun loadGEPrices(): Map<String, String> { fun loadGEPrices(): Map<String, String> {
return if (kondoExposed_useLiveGEPrices) { return if (useLiveGEPrices) {
try { try {
println("GroundItems: Loading Remote GE Prices") println("GroundItems: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json") val url = URL("https://cdn.2009scape.org/gedata/latest.json")
@ -325,7 +324,7 @@ open class plugin : Plugin() {
} else { } else {
try { try {
println("GroundItems: Loading Local GE Prices") println("GroundItems: Loading Local GE Prices")
BufferedReader(InputStreamReader(GroundItems.plugin::class.java.getResourceAsStream("item_configs.json"), StandardCharsets.UTF_8)) BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/item_configs.json"), StandardCharsets.UTF_8))
.useLines { lines -> .useLines { lines ->
val json = lines.joinToString("\n") val json = lines.joinToString("\n")
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" } val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }

View file

@ -5,4 +5,4 @@ Commands:\
::(tag,ignore)item ID\ ::(tag,ignore)item ID\
::(reset)groundconfig\ ::(reset)groundconfig\
Special thanks to Chisato for the original skeleton. Special thanks to Chisato for the original skeleton.
VERSION=1.2 VERSION=1.3

View file

@ -0,0 +1,166 @@
package KondoKit
import KondoKit.plugin.Companion.FIXED_HEIGHT
import KondoKit.plugin.Companion.FIXED_WIDTH
import plugin.api.API.IsHD
import rt4.GameShell.canvas
import rt4.GlRenderer
import rt4.SoftwareRaster
import java.awt.*
import java.awt.event.*
import java.awt.geom.AffineTransform
import java.awt.image.AffineTransformOp
import java.awt.image.BufferedImage
import java.awt.image.VolatileImage
class AltCanvas : Canvas() {
private var gameImage: VolatileImage? = null
private var op: AffineTransformOp? = null
private var transform: AffineTransform? = null
private var flippedImage: BufferedImage? = null
private var bufferImage = BufferedImage(FIXED_WIDTH, FIXED_HEIGHT, BufferedImage.TYPE_INT_BGR)
private var lastImageWidth = -1
private var lastImageHeight = -1
init {
isFocusable = true
requestFocusInWindow()
addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) = relayMouseEvent(e)
override fun mouseReleased(e: MouseEvent) = relayMouseEvent(e)
override fun mouseClicked(e: MouseEvent) = relayMouseEvent(e)
})
addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(e: MouseEvent) = relayMouseEvent(e)
override fun mouseDragged(e: MouseEvent) = relayMouseEvent(e)
})
addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) = relayKeyEvent { it.keyPressed(e) }
override fun keyReleased(e: KeyEvent) = relayKeyEvent { it.keyReleased(e) }
override fun keyTyped(e: KeyEvent) = relayKeyEvent { it.keyTyped(e) }
})
addMouseWheelListener(MouseWheelListener { e -> relayMouseWheelEvent(e) })
}
override fun update(g: Graphics) = paint(g)
override fun addNotify() {
super.addNotify()
createBufferStrategy(2) // Double-buffering
validateGameImage()
}
private fun validateGameImage() {
val gc = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration
gameImage?.let {
when (it.validate(gc)) {
VolatileImage.IMAGE_INCOMPATIBLE -> createGameImage(gc)
VolatileImage.IMAGE_RESTORED -> renderGameImage()
}
} ?: createGameImage(gc)
}
private fun createGameImage(gc: GraphicsConfiguration) {
gameImage = gc.createCompatibleVolatileImage(FIXED_WIDTH, FIXED_HEIGHT, Transparency.OPAQUE)
renderGameImage()
}
private fun renderGameImage() {
gameImage?.createGraphics()?.apply {
color = Color.DARK_GRAY
fillRect(0, 0, gameImage!!.width, gameImage!!.height)
color = Color.BLACK
drawString("KondoKit Scaled Fixed Canvas", 20, 20)
dispose()
}
}
override fun paint(g: Graphics) {
bufferStrategy?.let { strategy ->
val g2d = strategy.drawGraphics as? Graphics2D ?: return
g2d.color = Color.BLACK
g2d.fillRect(0, 0, width, height)
gameImage?.let { image ->
val scale = minOf(width.toDouble() / image.width, height.toDouble() / image.height)
val x = ((width - image.width * scale) / 2).toInt()
val y = ((height - image.height * scale) / 2).toInt()
g2d.drawImage(image, x, y, (image.width * scale).toInt(), (image.height * scale).toInt(), null)
}
g2d.dispose() // Release the graphics context
strategy.show() // Display the buffer
}
}
private fun relayMouseEvent(e: MouseEvent) {
requestFocusInWindow()
val scale = minOf(width.toDouble() / gameImage!!.width, height.toDouble() / gameImage!!.height)
val xOffset = ((width - gameImage!!.width * scale) / 2)
val yOffset = ((height - gameImage!!.height * scale) / 2)
val adjustedX = ((e.x - xOffset) / scale).toInt().coerceIn(0, gameImage!!.width - 1)
val adjustedY = ((e.y - yOffset) / scale).toInt().coerceIn(0, gameImage!!.height - 1)
canvas.dispatchEvent(MouseEvent(this, e.id, e.`when`, e.modifiersEx, adjustedX, adjustedY, e.clickCount, e.isPopupTrigger, e.button))
}
private fun relayKeyEvent(action: (KeyListener) -> Unit) {
for (listener in canvas.keyListeners) action(listener)
}
private fun relayMouseWheelEvent(e: MouseWheelEvent) {
val scale = minOf(width.toDouble() / gameImage!!.width, height.toDouble() / gameImage!!.height)
val xOffset = ((width - gameImage!!.width * scale) / 2)
val yOffset = ((height - gameImage!!.height * scale) / 2)
val adjustedX = ((e.x - xOffset) / scale).toInt().coerceIn(0, gameImage!!.width - 1)
val adjustedY = ((e.y - yOffset) / scale).toInt().coerceIn(0, gameImage!!.height - 1)
canvas.dispatchEvent(MouseWheelEvent(this, e.id, e.`when`, e.modifiersEx, adjustedX, adjustedY, e.clickCount, e.isPopupTrigger, e.scrollType, e.scrollAmount, e.wheelRotation))
}
fun updateGameImage() {
if (IsHD()) renderGlRaster() else renderSoftwareRaster()
repaint()
}
private fun renderGlRaster() {
val width = gameImage!!.width
val height = gameImage!!.height
bufferImage.setRGB(0, 0, width, height, GlRenderer.pixelData, 0, width)
if (width != lastImageWidth || height != lastImageHeight) {
transform = AffineTransform.getScaleInstance(1.0, -1.0).apply {
translate(0.0, -height.toDouble())
}
op = AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR)
flippedImage = BufferedImage(width, height, bufferImage.type)
lastImageWidth = width
lastImageHeight = height
}
op!!.filter(bufferImage, flippedImage)
gameImage?.createGraphics()?.apply {
drawImage(flippedImage, 0, 0, null)
dispose()
}
}
private fun renderSoftwareRaster() {
gameImage?.createGraphics()?.apply {
SoftwareRaster.frameBuffer.draw(this)
dispose()
}
}
}

View file

@ -1,10 +1,159 @@
package KondoKit package KondoKit
import java.awt.Color import rt4.GameShell
import java.awt.Dimension import java.awt.*
import javax.swing.JPanel import java.awt.event.MouseListener
import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.util.*
import java.util.Timer
import javax.swing.*
object Helpers { object Helpers {
fun convertValue(type: Class<*>, genericType: Type?, value: String): Any {
return when {
type == Int::class.java -> value.toInt()
type == Double::class.java -> value.toDouble()
type == Boolean::class.java -> value.toBoolean()
type == Color::class.java -> convertToColor(value)
type == List::class.java && genericType is ParameterizedType -> {
val actualTypeArgument = genericType.actualTypeArguments.firstOrNull()
when {
value.isBlank() -> emptyList<Any>() // Handle empty string by returning an empty list
actualTypeArgument == Int::class.javaObjectType -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim().toInt() }
actualTypeArgument == String::class.java -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim() }
else -> throw IllegalArgumentException("Unsupported List type: $actualTypeArgument")
}
}
else -> value // Default to String
}
}
fun showToast(
parentComponent: Component?,
message: String,
messageType: Int = JOptionPane.INFORMATION_MESSAGE
) {
SwingUtilities.invokeLater {
val toast = JWindow()
toast.type = Window.Type.POPUP
toast.background = Color(0, 0, 0, 0)
val panel = JPanel()
panel.isOpaque = false
panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS)
val label = JLabel(message)
label.foreground = Color.WHITE
label.background = when (messageType) {
JOptionPane.ERROR_MESSAGE -> Color(220, 20, 60, 230) // Crimson for errors
JOptionPane.INFORMATION_MESSAGE -> Color(0, 128, 0, 230) // Green for success
JOptionPane.WARNING_MESSAGE -> Color(255, 165, 0, 230) // Orange for warnings
else -> Color(0, 0, 0, 170) // Default semi-transparent black
}
label.isOpaque = true
label.border = BorderFactory.createEmptyBorder(10, 20, 10, 20)
label.maximumSize = Dimension(242, 50)
label.preferredSize = Dimension(242, 50)
panel.add(label)
toast.contentPane.add(panel)
toast.pack()
// Adjust for parent component location if it exists
if (parentComponent != null && GameShell.canvas.isShowing) {
val parentLocation = parentComponent.locationOnScreen
val x = parentLocation.x
val y = GameShell.canvas.locationOnScreen.y
toast.setLocation(x, y)
} else {
// Fallback to screen center if no parent is provided
val screenSize = Toolkit.getDefaultToolkit().screenSize
val x = (screenSize.width - toast.width) / 2
val y = screenSize.height - toast.height - 50
toast.setLocation(x, y)
}
toast.isVisible = true
Timer().schedule(object : TimerTask() {
override fun run() {
SwingUtilities.invokeLater {
toast.isVisible = false
toast.dispose()
}
}
}, 2000)
}
}
private fun convertToColor(value: String): Color {
return Color.decode(value)
}
fun colorToHex(color: Color): String {
return "#%02x%02x%02x".format(color.red, color.green, color.blue)
}
fun colorToIntArray(color: Color): IntArray {
return intArrayOf(color.red, color.green, color.blue)
}
interface FieldObserver {
fun onFieldChange(field: Field, newValue: Any?)
}
fun addMouseListenerToAll(container: Container, listener: MouseListener) {
// Recursively go through all components within the container
for (component in container.components) {
// Add the passed MouseListener to the component
if (component is JComponent || component is Canvas) {
component.addMouseListener(listener)
}
// If the component is a container, recursively call this function
if (component is Container) {
addMouseListenerToAll(component, listener)
}
}
}
class FieldNotifier(private val plugin: Any) {
private val observers = mutableListOf<FieldObserver>()
private fun notifyFieldChange(field: Field, newValue: Any?) {
for (observer in observers) {
observer.onFieldChange(field, newValue)
}
}
fun setFieldValue(field: Field, value: Any?) {
field.isAccessible = true
field.set(plugin, value)
notifyFieldChange(field, value)
try {
val onUpdateMethod = plugin::class.java.getMethod("OnKondoValueUpdated")
onUpdateMethod.invoke(plugin)
} catch (e: NoSuchMethodException) {
// The method doesn't exist
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun getSpriteId(skillId: Int) : Int { fun getSpriteId(skillId: Int) : Int {
return when (skillId) { return when (skillId) {
0 -> 197 0 -> 197
@ -35,9 +184,15 @@ object Helpers {
} }
} }
fun showAlert(message: String, title: String, type: Int){
JOptionPane.showMessageDialog(null, message, title, type)
}
fun formatHtmlLabelText(text1: String, color1: Color, text2: String, color2: Color): String { fun formatHtmlLabelText(text1: String, color1: Color, text2: String, color2: Color): String {
return "<html><span style='color:rgb(${color1.red},${color1.green},${color1.blue});'>$text1</span>" + return "<html><div style='white-space:nowrap;'>" +
"<span style='color:rgb(${color2.red},${color2.green},${color2.blue});'>$text2</span></html>" "<span style='color:rgb(${color1.red},${color1.green},${color1.blue});'>$text1</span>" +
"<span style='color:rgb(${color2.red},${color2.green},${color2.blue});'>$text2</span>" +
"</div></html>"
} }
fun formatNumber(value: Int): String { fun formatNumber(value: Int): String {
@ -78,13 +233,4 @@ object Helpers {
else -> Color(128, 128, 128) // Default grey for unhandled skill IDs else -> Color(128, 128, 128) // Default grey for unhandled skill IDs
} }
} }
class Spacer(width: Int = 0, height: Int = 0) : JPanel() {
init {
preferredSize = Dimension(width, height)
maximumSize = preferredSize
minimumSize = preferredSize
isOpaque = false
}
}
} }

View file

@ -1,9 +1,19 @@
package KondoKit package KondoKit
import KondoKit.Constants.COLOR_BACKGROUND_DARK 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.getSpriteId
import KondoKit.Helpers.showToast
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite 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.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 com.google.gson.Gson
import plugin.api.API import plugin.api.API
import rt4.Sprites import rt4.Sprites
@ -16,9 +26,11 @@ import java.awt.event.MouseEvent
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL import java.net.URL
import javax.swing.* import javax.swing.*
import javax.swing.border.MatteBorder import javax.swing.border.MatteBorder
import kotlin.math.floor
object Constants { object Constants {
// Sprite IDs // Sprite IDs
@ -28,47 +40,49 @@ object Constants {
const val LVL_BAR_SPRITE = 898 const val LVL_BAR_SPRITE = 898
// Dimensions // Dimensions
val SEARCH_FIELD_DIMENSION = Dimension(270, 30) val SEARCH_FIELD_DIMENSION = Dimension(230, 30)
val ICON_DIMENSION_SMALL = Dimension(12, 12) val ICON_DIMENSION_SMALL = Dimension(12, 12)
val ICON_DIMENSION_MEDIUM = Dimension(18, 20)
val ICON_DIMENSION_LARGE = Dimension(30, 30) val ICON_DIMENSION_LARGE = Dimension(30, 30)
val HISCORE_PANEL_DIMENSION = Dimension(270, 400) val HISCORE_PANEL_DIMENSION = Dimension(230, 500)
val FILTER_PANEL_DIMENSION = Dimension(270, 30) val FILTER_PANEL_DIMENSION = Dimension(230, 30)
val SKILLS_PANEL_DIMENSION = Dimension(300, 300) val SKILLS_PANEL_DIMENSION = Dimension(230, 290)
val TOTAL_COMBAT_PANEL_DIMENSION = Dimension(270, 30) val TOTAL_COMBAT_PANEL_DIMENSION = Dimension(230, 30)
val SKILL_PANEL_DIMENSION = Dimension(90, 35) val SKILL_PANEL_DIMENSION = Dimension(76, 35)
val IMAGE_CANVAS_DIMENSION = Dimension(20, 20) val IMAGE_CANVAS_DIMENSION = Dimension(20, 20)
val NUMBER_LABEL_DIMENSION = Dimension(30, 20) val SKILL_SPRITE_DIMENSION = Dimension(14, 14)
val NUMBER_LABEL_DIMENSION = Dimension(20, 20)
// Colors // Colors
val COLOR_BACKGROUND_DARK = Color(27, 27, 27) val COLOR_BACKGROUND_DARK = WIDGET_COLOR
val COLOR_BACKGROUND_MEDIUM = Color(37, 37, 37) val COLOR_BACKGROUND_MEDIUM = VIEW_BACKGROUND_COLOR
val COLOR_BACKGROUND_LIGHT = Color(43, 43, 43) val COLOR_FOREGROUND_LIGHT = POPUP_FOREGROUND
val COLOR_FOREGROUND_LIGHT = Color(200, 200, 200)
val COLOR_RED = Color.RED
val COLOR_SKILL_PANEL = Color(60, 60, 60)
// Fonts // Fonts
val FONT_ARIAL_PLAIN_14 = Font("Arial", Font.PLAIN, 14) val FONT_ARIAL_PLAIN_14 = Font("Arial", Font.PLAIN, 14)
val FONT_ARIAL_PLAIN_12 = Font("Arial", Font.PLAIN, 12) val FONT_ARIAL_PLAIN_12 = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val FONT_ARIAL_BOLD_12 = Font("Arial", Font.BOLD, 12) 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 = "" var text: String = ""
object HiscoresView { object HiscoresView {
const val VIEW_NAME = "HISCORE_SEARCH_VIEW"
var hiScoreView: JPanel? = null
class CustomSearchField(private val hiscoresPanel: JPanel) : Canvas() { class CustomSearchField(private val hiscoresPanel: JPanel) : Canvas() {
private var cursorVisible: Boolean = true private var cursorVisible: Boolean = true
private val gson = Gson() private val gson = Gson()
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.MAG_SPRITE)) private val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.MAG_SPRITE))
val imageCanvas = bufferedImageSprite.let { private val imageCanvas = bufferedImageSprite.let {
ImageCanvas(it).apply { ImageCanvas(it).apply {
preferredSize = Constants.ICON_DIMENSION_SMALL preferredSize = Constants.ICON_DIMENSION_SMALL
size = preferredSize size = preferredSize
minimumSize = preferredSize minimumSize = preferredSize
maximumSize = preferredSize maximumSize = preferredSize
fillColor = COLOR_BACKGROUND_DARK
} }
} }
@ -96,7 +110,9 @@ object HiscoresView {
} else { } else {
text += e.keyChar text += e.keyChar
} }
repaint() SwingUtilities.invokeLater {
repaint()
}
} }
override fun keyPressed(e: KeyEvent) { override fun keyPressed(e: KeyEvent) {
if (e.isControlDown) { if (e.isControlDown) {
@ -109,7 +125,9 @@ object HiscoresView {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
text += pasteText text += pasteText
repaint() SwingUtilities.invokeLater {
repaint()
}
} }
} }
} }
@ -120,15 +138,19 @@ object HiscoresView {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (e.x > width - 20 && e.y < 20) { if (e.x > width - 20 && e.y < 20) {
text = "" text = ""
repaint() SwingUtilities.invokeLater {
repaint()
}
} }
} }
}) })
Timer(500) { Timer(500) {
cursorVisible = !cursorVisible cursorVisible = !cursorVisible
if(plugin.StateManager.focusedView == "HISCORE_SEARCH_VIEW") if(focusedView == VIEW_NAME)
repaint() SwingUtilities.invokeLater {
repaint()
}
}.start() }.start()
} }
@ -140,7 +162,7 @@ object HiscoresView {
val fm = g.fontMetrics val fm = g.fontMetrics
val cursorX = fm.stringWidth(text) + 30 val cursorX = fm.stringWidth(text) + 30
imageCanvas?.let { canvas -> imageCanvas.let { canvas ->
val imgG = g.create(5, 5, canvas.width, canvas.height) val imgG = g.create(5, 5, canvas.width, canvas.height)
canvas.paint(imgG) canvas.paint(imgG)
imgG.dispose() imgG.dispose()
@ -153,14 +175,16 @@ object HiscoresView {
} }
if (text.isNotEmpty()) { if (text.isNotEmpty()) {
g.color = Constants.COLOR_RED g.color = Color.RED
g.drawString("x", width - 20, 20) g.drawString("x", width - 20, 20)
} }
} }
fun searchPlayer(username: String) { fun searchPlayer(username: String) {
text = username text = username.replace(" ", "_")
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${username.toLowerCase()}" val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${text.toLowerCase()}"
updateHiscoresView(null, "Searching...")
Thread { Thread {
try { try {
@ -168,6 +192,10 @@ object HiscoresView {
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET" connection.requestMethod = "GET"
// If a request take longer than 5 seconds timeout.
connection.connectTimeout = 5000
connection.readTimeout = 5000
val responseCode = connection.responseCode val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream)) val reader = BufferedReader(InputStreamReader(connection.inputStream))
@ -179,27 +207,45 @@ object HiscoresView {
} }
} else { } else {
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
showError("Player not found!") showToast(hiscoresPanel, "Player not found!", JOptionPane.ERROR_MESSAGE)
} }
} }
} catch (e: Exception) { } catch (e: SocketTimeoutException) {
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
showError("Error fetching data!") 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() }.start()
} }
private fun updatePlayerData(jsonResponse: String, username: String) { private fun updatePlayerData(jsonResponse: String, username: String) {
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java) val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
updateHiscoresView(hiscoresResponse, username) updateHiscoresView(hiscoresResponse, username)
} }
private fun updateHiscoresView(data: HiscoresResponse, username: String) { private fun updateHiscoresView(data: HiscoresResponse?, username: String) {
val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel
val ironMode = data.info.iron_mode
playerNameLabel?.removeAll() // Clear previous components 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") { if (ironMode != "0") {
val ironmanBufferedImage = getBufferedImageFromSprite(Sprites.nameIcons[Constants.IRONMAN_SPRITE + ironMode.toInt() - 1]) val ironmanBufferedImage = getBufferedImageFromSprite(Sprites.nameIcons[Constants.IRONMAN_SPRITE + ironMode.toInt() - 1])
@ -213,11 +259,14 @@ object HiscoresView {
playerNameLabel?.add(imageCanvas) playerNameLabel?.add(imageCanvas)
} }
val nameLabel = JLabel(username, JLabel.CENTER).apply { 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 font = Constants.FONT_ARIAL_BOLD_12
foreground = Constants.COLOR_FOREGROUND_LIGHT foreground = Constants.COLOR_FOREGROUND_LIGHT
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
} }
playerNameLabel?.add(nameLabel) playerNameLabel?.add(nameLabel)
playerNameLabel?.revalidate() playerNameLabel?.revalidate()
@ -266,20 +315,16 @@ object HiscoresView {
summoning: Int, summoning: Int,
isMemberWorld: Boolean isMemberWorld: Boolean
): Double { ): Double {
val base = (defence + hitpoints + Math.floor(prayer.toDouble() / 2)) * 0.25 val base = (defence + hitpoints + floor(prayer.toDouble() / 2)) * 0.25
val melee = (attack + strength) * 0.325 val melee = (attack + strength) * 0.325
val range = Math.floor(ranged * 1.5) * 0.325 val range = floor(ranged * 1.5) * 0.325
val mage = Math.floor(magic * 1.5) * 0.325 val mage = floor(magic * 1.5) * 0.325
val maxCombatType = maxOf(melee, range, mage) val maxCombatType = maxOf(melee, range, mage)
val summoningFactor = if (isMemberWorld) Math.floor(summoning.toDouble() / 8) else 0.0 val summoningFactor = if (isMemberWorld) floor(summoning.toDouble() / 8) else 0.0
return Math.round((base + maxCombatType + summoningFactor) * 1000.0) / 1000.0 return Math.round((base + maxCombatType + summoningFactor) * 1000.0) / 1000.0
} }
private fun showError(message: String) {
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE)
}
private fun findComponentByName(container: Container, name: String): Component? { private fun findComponentByName(container: Container, name: String): Component? {
for (component in container.components) { for (component in container.components) {
if (name == component.name) { if (name == component.name) {
@ -296,10 +341,10 @@ object HiscoresView {
} }
} }
fun createHiscoreSearchView(): JPanel { fun createHiscoreSearchView() {
val hiscorePanel = JPanel().apply { val hiscorePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS) layout = BoxLayout(this, BoxLayout.Y_AXIS)
name = "HISCORE_SEARCH_VIEW" name = VIEW_NAME
background = Constants.COLOR_BACKGROUND_MEDIUM background = Constants.COLOR_BACKGROUND_MEDIUM
preferredSize = Constants.HISCORE_PANEL_DIMENSION preferredSize = Constants.HISCORE_PANEL_DIMENSION
maximumSize = preferredSize maximumSize = preferredSize
@ -324,14 +369,13 @@ object HiscoresView {
add(searchFieldWrapper) add(searchFieldWrapper)
} }
hiscorePanel.add(Helpers.Spacer(height = 10)) hiscorePanel.add(Box.createVerticalStrut(10))
hiscorePanel.add(searchPanel) hiscorePanel.add(searchPanel)
hiscorePanel.add(Helpers.Spacer(height = 10)) hiscorePanel.add(Box.createVerticalStrut(10))
// Adding the player name panel in place of the filterPanel
val playerNamePanel = JPanel().apply { val playerNamePanel = JPanel().apply {
layout = FlowLayout(FlowLayout.CENTER) layout = GridBagLayout() // This will center the JLabel both vertically and horizontally
background = VIEW_BACKGROUND_COLOR background = TOOLTIP_BACKGROUND.darker()
preferredSize = Constants.FILTER_PANEL_DIMENSION preferredSize = Constants.FILTER_PANEL_DIMENSION
maximumSize = preferredSize maximumSize = preferredSize
minimumSize = preferredSize minimumSize = preferredSize
@ -339,7 +383,7 @@ object HiscoresView {
} }
hiscorePanel.add(playerNamePanel) hiscorePanel.add(playerNamePanel)
hiscorePanel.add(Helpers.Spacer(height = 10)) hiscorePanel.add(Box.createVerticalStrut(10))
val skillsPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply { val skillsPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
background = Constants.COLOR_BACKGROUND_MEDIUM background = Constants.COLOR_BACKGROUND_MEDIUM
@ -348,10 +392,10 @@ object HiscoresView {
minimumSize = preferredSize minimumSize = preferredSize
} }
for (i in 0 until 24) { for (i in SKILL_DISPLAY_ORDER) {
val skillPanel = JPanel().apply { val skillPanel = JPanel().apply {
layout = BorderLayout() layout = BorderLayout()
background = Constants.COLOR_SKILL_PANEL background = COLOR_BACKGROUND_DARK
preferredSize = Constants.SKILL_PANEL_DIMENSION preferredSize = Constants.SKILL_PANEL_DIMENSION
maximumSize = preferredSize maximumSize = preferredSize
minimumSize = preferredSize minimumSize = preferredSize
@ -362,8 +406,9 @@ object HiscoresView {
val imageCanvas = bufferedImageSprite.let { val imageCanvas = bufferedImageSprite.let {
ImageCanvas(it).apply { ImageCanvas(it).apply {
preferredSize = Constants.IMAGE_CANVAS_DIMENSION preferredSize = SKILL_SPRITE_DIMENSION
size = Constants.IMAGE_CANVAS_DIMENSION size = SKILL_SPRITE_DIMENSION
fillColor = COLOR_BACKGROUND_DARK
} }
} }
@ -376,7 +421,7 @@ object HiscoresView {
} }
val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply { val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply {
background = Constants.COLOR_BACKGROUND_DARK background = COLOR_BACKGROUND_DARK
add(imageCanvas) add(imageCanvas)
add(numberLabel) add(numberLabel)
} }
@ -394,9 +439,10 @@ object HiscoresView {
minimumSize = preferredSize minimumSize = preferredSize
} }
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE)); val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE))
val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply { val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply {
fillColor = COLOR_BACKGROUND_DARK
preferredSize = Constants.ICON_DIMENSION_LARGE preferredSize = Constants.ICON_DIMENSION_LARGE
size = Constants.ICON_DIMENSION_LARGE size = Constants.ICON_DIMENSION_LARGE
} }
@ -418,6 +464,7 @@ object HiscoresView {
val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE)) val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE))
val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply { val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply {
fillColor = COLOR_BACKGROUND_DARK
preferredSize = Constants.ICON_DIMENSION_LARGE preferredSize = Constants.ICON_DIMENSION_LARGE
size = Constants.ICON_DIMENSION_LARGE size = Constants.ICON_DIMENSION_LARGE
} }
@ -431,7 +478,7 @@ object HiscoresView {
} }
val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply { val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
background = Constants.COLOR_BACKGROUND_DARK background = COLOR_BACKGROUND_DARK
add(combatLevelIcon) add(combatLevelIcon)
add(combatLevelLabel) add(combatLevelLabel)
} }
@ -439,8 +486,9 @@ object HiscoresView {
totalCombatPanel.add(totalLevelPanel) totalCombatPanel.add(totalLevelPanel)
totalCombatPanel.add(combatLevelPanel) totalCombatPanel.add(combatLevelPanel)
hiscorePanel.add(totalCombatPanel) hiscorePanel.add(totalCombatPanel)
hiscorePanel.add(Box.createVerticalStrut(10))
return hiscorePanel hiScoreView = hiscorePanel
} }
data class HiscoresResponse( data class HiscoresResponse(

View file

@ -1,5 +1,6 @@
package KondoKit package KondoKit
import KondoKit.plugin.Companion.WIDGET_COLOR
import java.awt.Canvas import java.awt.Canvas
import java.awt.Color import java.awt.Color
import java.awt.Dimension import java.awt.Dimension
@ -8,31 +9,25 @@ import java.awt.image.BufferedImage
class ImageCanvas(private val image: BufferedImage) : Canvas() { class ImageCanvas(private val image: BufferedImage) : Canvas() {
var fillColor: Color = WIDGET_COLOR
init { init {
// Manually set the alpha value to 255 (fully opaque) only for pixels that are not fully transparent
val width = image.width val width = image.width
val height = image.height val height = image.height
for (y in 0 until height) { for (y in 0 until height) {
for (x in 0 until width) { for (x in 0 until width) {
// Retrieve the current pixel color
val color = image.getRGB(x, y) val color = image.getRGB(x, y)
// Check if the pixel is not fully transparent (i.e., color is not 0)
if (color != 0) { if (color != 0) {
// Ensure the alpha is set to 255 (fully opaque)
val newColor = (color and 0x00FFFFFF) or (0xFF shl 24) val newColor = (color and 0x00FFFFFF) or (0xFF shl 24)
// Set the pixel with the updated color
image.setRGB(x, y, newColor) image.setRGB(x, y, newColor)
} }
} }
} }
} }
override fun paint(g: Graphics) { override fun paint(g: Graphics) {
super.paint(g) super.paint(g)
g.color = Color(27, 27, 27) g.color = fillColor
g.fillRect(0, 0, width, height) g.fillRect(0, 0, width, height)
g.drawImage(image, 0, 0, width, height, this) g.drawImage(image, 0, 0, width, height, this)
} }

View file

@ -1,100 +0,0 @@
package KondoKit
import java.awt.Color
import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/*
This is used for the runtime editing of plugin variables.
To expose fields name them starting with `kondoExposed_`
When they are applied this will trigger an invoke of OnKondoValueUpdated()
if it is implemented. Check GroundItems plugin for an example.
*/
object KondoKitUtils {
const val KONDO_PREFIX = "kondoExposed_"
fun isKondoExposed(field: Field): Boolean {
return field.name.startsWith(KONDO_PREFIX)
}
fun getKondoExposedFields(instance: Any): List<Field> {
val exposedFields: MutableList<Field> = ArrayList()
for (field in instance.javaClass.declaredFields) {
if (isKondoExposed(field)) {
exposedFields.add(field)
}
}
return exposedFields
}
fun convertValue(type: Class<*>, genericType: Type?, value: String): Any {
return when {
type == Int::class.java -> value.toInt()
type == Double::class.java -> value.toDouble()
type == Boolean::class.java -> value.toBoolean()
type == Color::class.java -> convertToColor(value)
type == List::class.java && genericType is ParameterizedType -> {
val actualTypeArgument = genericType.actualTypeArguments.firstOrNull()
when {
value.isBlank() -> emptyList<Any>() // Handle empty string by returning an empty list
actualTypeArgument == Int::class.javaObjectType -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim().toInt() }
actualTypeArgument == String::class.java -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim() }
else -> throw IllegalArgumentException("Unsupported List type: $actualTypeArgument")
}
}
else -> value // Default to String
}
}
fun convertToColor(value: String): Color {
val color = Color.decode(value) // Assumes value is in format "#RRGGBB" or "0xRRGGBB"
return color
}
fun colorToHex(color: Color): String {
return "#%02x%02x%02x".format(color.red, color.green, color.blue)
}
fun colorToIntArray(color: Color): IntArray {
return intArrayOf(color.red, color.green, color.blue)
}
interface FieldObserver {
fun onFieldChange(field: Field, newValue: Any?)
}
class FieldNotifier(private val plugin: Any) {
private val observers = mutableListOf<FieldObserver>()
fun addObserver(observer: FieldObserver) {
observers.add(observer)
}
fun notifyFieldChange(field: Field, newValue: Any?) {
for (observer in observers) {
observer.onFieldChange(field, newValue)
}
}
fun setFieldValue(field: Field, value: Any?) {
field.isAccessible = true
field.set(plugin, value)
notifyFieldChange(field, value)
try {
val onUpdateMethod = plugin::class.java.getMethod("OnKondoValueUpdated")
onUpdateMethod.invoke(plugin)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View file

@ -1,19 +1,25 @@
package KondoKit package KondoKit
import KondoKit.Helpers.addMouseListenerToAll
import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.XPTrackerView.wrappedWidget import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.plugin.Companion.POPUP_BACKGROUND
import KondoKit.plugin.Companion.POPUP_FOREGROUND
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.TOTAL_XP_WIDGET_SIZE
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import plugin.api.API import plugin.api.API
import rt4.NpcTypeList import rt4.*
import rt4.ObjStackNode
import rt4.Player
import rt4.SceneGraph
import java.awt.* import java.awt.*
import java.awt.Font
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
@ -22,19 +28,23 @@ import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.DecimalFormat import java.text.DecimalFormat
import javax.swing.* import javax.swing.*
import kotlin.math.ceil
object LootTrackerView { object LootTrackerView {
private const val SNAPSHOT_LIFESPAN = 10 private const val SNAPSHOT_LIFESPAN = 10
const val BAG_ICON = 900; const val BAG_ICON = 900
val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>() val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>()
var gePriceMap = loadGEPrices() var gePriceMap = loadGEPrices()
const val VIEW_NAME = "LOOT_TRACKER_VIEW"
private val lootItemPanels = mutableMapOf<String, MutableMap<Int, Int>>() private val lootItemPanels = mutableMapOf<String, MutableMap<Int, Int>>()
private val npcKillCounts = mutableMapOf<String, Int>() private val npcKillCounts = mutableMapOf<String, Int>()
private var totalTrackerWidget: XPWidget? = null private var totalTrackerWidget: XPWidget? = null
var lastConfirmedKillNpcId = -1; var lastConfirmedKillNpcId = -1
private var customToolTipWindow: JWindow? = null
var lootTrackerView: JPanel? = null
fun loadGEPrices(): Map<String, String> { fun loadGEPrices(): Map<String, String> {
return if (plugin.kondoExposed_useLiveGEPrices) { return if (plugin.useLiveGEPrices) {
try { try {
println("LootTracker: Loading Remote GE Prices") println("LootTracker: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json") val url = URL("https://cdn.2009scape.org/gedata/latest.json")
@ -69,7 +79,7 @@ object LootTrackerView {
} else { } else {
try { try {
println("LootTracker: Loading Local GE Prices") println("LootTracker: Loading Local GE Prices")
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("item_configs.json"), StandardCharsets.UTF_8)) BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/item_configs.json"), StandardCharsets.UTF_8))
.useLines { lines -> .useLines { lines ->
val json = lines.joinToString("\n") val json = lines.joinToString("\n")
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" } val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
@ -94,19 +104,37 @@ object LootTrackerView {
fun createLootTrackerView(): JPanel { fun createLootTrackerView() {
return JPanel().apply { lootTrackerView = JPanel().apply {
layout = FlowLayout(FlowLayout.CENTER, 0, 5) layout = BoxLayout(this, BoxLayout.Y_AXIS) // Use BoxLayout on Y axis to stack widgets vertically
background = VIEW_BACKGROUND_COLOR background = VIEW_BACKGROUND_COLOR
preferredSize = Dimension(270, 700)
maximumSize = Dimension(270, 700)
minimumSize = Dimension(270, 700)
add(Box.createVerticalStrut(5)) add(Box.createVerticalStrut(5))
totalTrackerWidget = createTotalLootWidget() totalTrackerWidget = createTotalLootWidget()
add(wrappedWidget(totalTrackerWidget!!.panel))
add(Helpers.Spacer(height = 15)) val wrapped = wrappedWidget(totalTrackerWidget!!.container)
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)
add(wrapped)
add(Box.createVerticalStrut(8))
revalidate() revalidate()
repaint() if(focusedView == VIEW_NAME)
repaint()
} }
} }
@ -122,7 +150,7 @@ object LootTrackerView {
totalTrackerWidget?.let { totalTrackerWidget?.let {
it.previousXp += newVal it.previousXp += newVal
it.xpPerHourLabel.text = formatHtmlLabelText("Total Value: ", primaryColor, formatValue(it.previousXp) + " gp", secondaryColor) it.xpPerHourLabel.text = formatHtmlLabelText("Total Value: ", primaryColor, formatValue(it.previousXp) + " gp", secondaryColor)
it.panel.repaint() it.container.repaint()
} }
} }
@ -132,12 +160,12 @@ object LootTrackerView {
val l2 = createLabel(formatHtmlLabelText("Total Count: ", primaryColor, "0", secondaryColor)) val l2 = createLabel(formatHtmlLabelText("Total Count: ", primaryColor, "0", secondaryColor))
return XPWidget( return XPWidget(
skillId = -1, skillId = -1,
panel = createWidgetPanel(bufferedImageSprite,l2,l1), container = createWidgetPanel(bufferedImageSprite,l2,l1),
xpGainedLabel = l2, xpGainedLabel = l2,
xpLeftLabel = JLabel(), xpLeftLabel = JLabel(),
actionsRemainingLabel = JLabel(), actionsRemainingLabel = JLabel(),
xpPerHourLabel = l1, xpPerHourLabel = l1,
progressBar = ProgressBar(0.0, Color(150, 50, 50)), progressBar = ProgressBar(0.0, Color(0,0,0)), // unused.
totalXpGained = 0, totalXpGained = 0,
startTime = System.currentTimeMillis(), startTime = System.currentTimeMillis(),
previousXp = 0 previousXp = 0
@ -146,16 +174,19 @@ object LootTrackerView {
private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): Panel { private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): Panel {
val imageCanvas = ImageCanvas(bufferedImageSprite).apply { val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
size = Dimension(width, height) preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
minimumSize = preferredSize
maximumSize = preferredSize
size = preferredSize
background = WIDGET_COLOR background = WIDGET_COLOR
} }
val imageContainer = Panel(FlowLayout()).apply { val imageContainer = Panel(BorderLayout()).apply {
background = WIDGET_COLOR background = WIDGET_COLOR
add(imageCanvas) add(imageCanvas, BorderLayout.NORTH)
} }
return Panel(BorderLayout(5, 5)).apply { return Panel(BorderLayout(5, 0)).apply {
background = WIDGET_COLOR background = WIDGET_COLOR
preferredSize = TOTAL_XP_WIDGET_SIZE preferredSize = TOTAL_XP_WIDGET_SIZE
add(imageContainer, BorderLayout.WEST) add(imageContainer, BorderLayout.WEST)
@ -164,7 +195,7 @@ object LootTrackerView {
} }
private fun createTextPanel(l1 : JLabel, l2: JLabel): Panel { private fun createTextPanel(l1 : JLabel, l2: JLabel): Panel {
return Panel(GridLayout(2, 1, 5, 5)).apply { return Panel(GridLayout(2, 1, 5, 0)).apply {
background = WIDGET_COLOR background = WIDGET_COLOR
add(l1) add(l1)
add(l2) add(l2)
@ -173,7 +204,7 @@ object LootTrackerView {
private fun createLabel(text: String): JLabel { private fun createLabel(text: String): JLabel {
return JLabel(text).apply { return JLabel(text).apply {
font = Font("Arial", Font.PLAIN, 11) font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
horizontalAlignment = JLabel.LEFT horizontalAlignment = JLabel.LEFT
} }
} }
@ -189,12 +220,19 @@ object LootTrackerView {
// Recalculate lootPanel size based on the number of unique items. // Recalculate lootPanel size based on the number of unique items.
val totalItems = lootItemPanels[npcName]?.size ?: 0 val totalItems = lootItemPanels[npcName]?.size ?: 0
val rowsNeeded = Math.ceil(totalItems / 6.0).toInt() val rowsNeeded = ceil(totalItems / 6.0).toInt()
val lootPanelHeight = rowsNeeded * 36 + (rowsNeeded - 1) val lootPanelHeight = rowsNeeded * (40)
lootPanel.preferredSize = Dimension(270, lootPanelHeight+10)
val size = Dimension(lootPanel.width,lootPanelHeight+32)
lootPanel.parent.preferredSize = size
lootPanel.parent.minimumSize = size
lootPanel.parent.maximumSize = size
lootPanel.parent.revalidate()
lootPanel.parent.repaint()
lootPanel.revalidate() lootPanel.revalidate()
lootPanel.repaint()
if(focusedView == VIEW_NAME)
lootPanel.repaint()
} }
} }
@ -204,27 +242,93 @@ object LootTrackerView {
} }
private fun createItemPanel(itemId: Int, quantity: Int): JPanel { private fun createItemPanel(itemId: Int, quantity: Int): JPanel {
val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 0, 0)) val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 1, 3153952))
return FixedSizePanel(Dimension(36, 32)).apply {
// Create the panel for the item
val itemPanel = FixedSizePanel(Dimension(36, 32)).apply {
preferredSize = Dimension(36, 32) preferredSize = Dimension(36, 32)
background = WIDGET_COLOR background = WIDGET_COLOR
minimumSize = preferredSize minimumSize = preferredSize
maximumSize = preferredSize maximumSize = preferredSize
add(ImageCanvas(bufferedImageSprite).apply {
val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
preferredSize = Dimension(36, 32) preferredSize = Dimension(36, 32)
background = WIDGET_COLOR background = WIDGET_COLOR
minimumSize = preferredSize minimumSize = preferredSize
maximumSize = preferredSize maximumSize = preferredSize
}, BorderLayout.CENTER) }
// Add the imageCanvas to the panel
add(imageCanvas, BorderLayout.CENTER)
// Put the itemId as a property for reference
putClientProperty("itemId", itemId) 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()
}
})
} }
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
val totalGePrice = gePricePerItem * quantity
val totalHaPrice = itemDef.cost * quantity
val geText = if (quantity > 1) " (${formatValue(gePricePerItem)} ea)" else ""
val haText = if (quantity > 1) " (${formatValue(itemDef.cost)} ea)" else ""
val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND)
val textColor = Helpers.colorToHex(secondaryColor)
val text = "<html><div style='color: "+textColor+"; background-color: "+bgColor+"; padding: 3px;'>" +
"${itemDef.name} x $quantity<br>" +
"GE: ${formatValue(totalGePrice)} ${geText}<br>" +
"HA: ${formatValue(totalHaPrice)} ${haText}</div></html>"
val _font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
if (customToolTipWindow == null) {
customToolTipWindow = JWindow().apply {
contentPane = JLabel(text).apply {
border = BorderFactory.createLineBorder(Color.BLACK)
isOpaque = true
background = TOOLTIP_BACKGROUND
foreground = Color.WHITE
font = _font
}
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
}
private fun updateItemPanelIcon(panel: JPanel, itemId: Int, quantity: Int) { private fun updateItemPanelIcon(panel: JPanel, itemId: Int, quantity: Int) {
panel.removeAll() panel.removeAll()
panel.add(createItemPanel(itemId, quantity).components[0], BorderLayout.CENTER) panel.add(createItemPanel(itemId, quantity).components[0], BorderLayout.CENTER)
panel.revalidate() panel.revalidate()
panel.repaint() if(focusedView == VIEW_NAME)
panel.repaint()
} }
private fun updateKillCountLabel(lootTrackerPanel: JPanel, npcName: String) { private fun updateKillCountLabel(lootTrackerPanel: JPanel, npcName: String) {
@ -246,9 +350,11 @@ object LootTrackerView {
?.apply { ?.apply {
val newValue = (getClientProperty("val") as? Int ?: 0) + valueOfNewDrops.toInt() val newValue = (getClientProperty("val") as? Int ?: 0) + valueOfNewDrops.toInt()
text = "${formatValue(newValue)} gp" text = "${formatValue(newValue)} gp"
foreground = primaryColor
putClientProperty("val", newValue) putClientProperty("val", newValue)
revalidate() revalidate()
repaint() if(focusedView == VIEW_NAME)
repaint()
} }
} }
} }
@ -276,7 +382,7 @@ object LootTrackerView {
if (newDrops.isNotEmpty()) { if (newDrops.isNotEmpty()) {
val npcName = NpcTypeList.get(npcId).name val npcName = NpcTypeList.get(npcId).name
lastConfirmedKillNpcId = npcId; lastConfirmedKillNpcId = npcId
handleNewDrops(npcName.toString(), newDrops, lootTrackerView) handleNewDrops(npcName.toString(), newDrops, lootTrackerView)
toRemove.add(npcId) toRemove.add(npcId)
} else if (snapshot.age >= SNAPSHOT_LIFESPAN) { } else if (snapshot.age >= SNAPSHOT_LIFESPAN) {
@ -308,11 +414,11 @@ object LootTrackerView {
private fun handleNewDrops(npcName: String, newDrops: Set<Item>, lootTrackerView: JPanel) { private fun handleNewDrops(npcName: String, newDrops: Set<Item>, lootTrackerView: JPanel) {
findLootItemsPanel(lootTrackerView, npcName)?.let { findLootItemsPanel(lootTrackerView, npcName)?.let {
} ?: run { } ?: run {
// Panel doesn't exist, so create and add it
lootTrackerView.add(createLootFrame(npcName)) lootTrackerView.add(createLootFrame(npcName))
lootTrackerView.add(Helpers.Spacer(height = 15)) lootTrackerView.add(Box.createVerticalStrut(8))
lootTrackerView.revalidate() lootTrackerView.revalidate()
lootTrackerView.repaint() if(focusedView == VIEW_NAME)
lootTrackerView.repaint()
} }
npcKillCounts[npcName] = npcKillCounts.getOrDefault(npcName, 0) + 1 npcKillCounts[npcName] = npcKillCounts.getOrDefault(npcName, 0) + 1
@ -321,7 +427,7 @@ object LootTrackerView {
newDrops.forEach { drop -> newDrops.forEach { drop ->
val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity
updateValueLabel(lootTrackerView, geValue.toString(), npcName) updateValueLabel(lootTrackerView, geValue.toString(), npcName)
addItemToLootPanel(lootTrackerView, drop, npcName) plugin.registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
updateTotalValue(geValue) updateTotalValue(geValue)
} }
} }
@ -330,29 +436,30 @@ object LootTrackerView {
val childFramePanel = JPanel().apply { val childFramePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS) layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR background = WIDGET_COLOR
minimumSize = Dimension(270, 0) minimumSize = Dimension(230, 0)
maximumSize = Dimension(270, 700) maximumSize = Dimension(230, 700)
name = "HELLO_WORLD"
} }
val labelPanel = JPanel(BorderLayout()).apply { val labelPanel = JPanel(BorderLayout()).apply {
background = Color(21, 21, 21) background = TITLE_BAR_COLOR
border = BorderFactory.createEmptyBorder(5, 5, 5, 5) border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
maximumSize = Dimension(270, 24) maximumSize = Dimension(230, 24)
minimumSize = maximumSize minimumSize = maximumSize
preferredSize = maximumSize preferredSize = maximumSize
} }
val killCount = npcKillCounts.getOrPut(npcName) { 0 } val killCount = npcKillCounts.getOrPut(npcName) { 0 }
val countLabel = JLabel(formatHtmlLabelText(npcName, secondaryColor, " x $killCount", primaryColor)).apply { val countLabel = JLabel(formatHtmlLabelText(npcName, secondaryColor, " x $killCount", primaryColor)).apply {
foreground = Color(200, 200, 200) foreground = secondaryColor
font = Font("Arial", Font.PLAIN, 12) font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
horizontalAlignment = JLabel.LEFT horizontalAlignment = JLabel.LEFT
name = "killCountLabel_$npcName" name = "killCountLabel_$npcName"
} }
val valueLabel = JLabel("0 gp").apply { val valueLabel = JLabel("0 gp").apply {
foreground = Color(200, 200, 200) foreground = secondaryColor
font = Font("Arial", Font.PLAIN, 12) font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
horizontalAlignment = JLabel.RIGHT horizontalAlignment = JLabel.RIGHT
name = "valueLabel_$npcName" name = "valueLabel_$npcName"
} }
@ -370,19 +477,120 @@ object LootTrackerView {
lootItemPanels[npcName] = mutableMapOf() lootItemPanels[npcName] = mutableMapOf()
// Determine number of items and adjust size of lootPanel
val totalItems = lootItemPanels[npcName]?.size ?: 0
val rowsNeeded = Math.ceil(totalItems / 6.0).toInt()
val lootPanelHeight = rowsNeeded * 36 + (rowsNeeded - 1) // Height per row = 36 + spacing
lootPanel.preferredSize = Dimension(270, lootPanelHeight+10)
childFramePanel.add(labelPanel) childFramePanel.add(labelPanel)
childFramePanel.add(lootPanel) childFramePanel.add(lootPanel)
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)
return childFramePanel 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 {
lootItemPanels[npcName]?.clear()
npcKillCounts[npcName] = 0
lootTrackerView?.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) {
// Nasty way to remove the Box.createVerticalStrut(8) after
// the lootpanel.
parent.remove(nextComponent)
}
}
parent.remove(toRemove)
parent.revalidate()
parent.repaint()
}
}
return popupMenu
}
private fun resetLootTrackerMenu(): 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 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 {
resetLootTracker()
}
}
return popupMenu
}
private fun resetLootTracker(){
lootTrackerView?.removeAll()
npcKillCounts.clear()
lootItemPanels.clear()
totalTrackerWidget = createTotalLootWidget()
val wrapped = wrappedWidget(totalTrackerWidget!!.container)
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)
lootTrackerView?.add(Box.createVerticalStrut(5))
lootTrackerView?.add(wrapped)
lootTrackerView?.add(Box.createVerticalStrut(8))
lootTrackerView?.revalidate()
lootTrackerView?.repaint()
}
class FixedSizePanel(private val fixedSize: Dimension) : JPanel() { class FixedSizePanel(private val fixedSize: Dimension) : JPanel() {
override fun getPreferredSize(): Dimension { override fun getPreferredSize(): Dimension {

View file

@ -1,19 +1,22 @@
package KondoKit package KondoKit
import KondoKit.plugin.Companion.PROGRESS_BAR_FILL
import KondoKit.plugin.Companion.secondaryColor
import java.awt.Canvas import java.awt.Canvas
import java.awt.Color import java.awt.Color
import java.awt.Dimension
import java.awt.Font import java.awt.Font
import java.awt.Graphics import java.awt.Graphics
class ProgressBar( class ProgressBar(
private var progress: Double, private var progress: Double,
private val barColor: Color, private val barColor: Color,
private var currentLevel: Int = 0, private var currentLevel: Int = 0,
private var nextLevel: Int = 1 private var nextLevel: Int = 1
) : Canvas() { ) : Canvas() {
init { init {
font = Font("Arial", Font.PLAIN, 12) font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
} }
override fun paint(g: Graphics) { override fun paint(g: Graphics) {
@ -25,22 +28,32 @@ class ProgressBar(
g.fillRect(0, 0, width, this.height) g.fillRect(0, 0, width, this.height)
// Draw the unfilled part of the progress bar // Draw the unfilled part of the progress bar
g.color = Color(100, 100, 100) g.color = PROGRESS_BAR_FILL
g.fillRect(width, 0, this.width - width, this.height) 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 // Draw the current level on the far left
g.color = Color(255, 255, 255) drawTextWithShadow(g, "Lvl. $currentLevel", 5, textY, secondaryColor)
g.drawString("Lvl. $currentLevel", 5, this.height / 2 + 4)
// Draw the percentage in the middle // Draw the percentage in the middle
val percentageText = String.format("%.2f%%", progress) val percentageText = String.format("%.2f%%", progress)
val percentageWidth = g.fontMetrics.stringWidth(percentageText) val percentageWidth = g.fontMetrics.stringWidth(percentageText)
g.drawString(percentageText, (this.width - percentageWidth) / 2, this.height / 2 + 4) drawTextWithShadow(g, percentageText, (this.width - percentageWidth) / 2, textY, secondaryColor)
// Draw the next level on the far right // Draw the next level on the far right
val nextLevelText = "Lvl. $nextLevel" val nextLevelText = "Lvl. $nextLevel"
val nextLevelWidth = g.fontMetrics.stringWidth(nextLevelText) val nextLevelWidth = g.fontMetrics.stringWidth(nextLevelText)
g.drawString(nextLevelText, this.width - nextLevelWidth - 5, this.height / 2 + 4) 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
}
override fun getMinimumSize(): Dimension {
return Dimension(220, 16) // Force the minimum height to 16px, width can be smaller
} }
fun updateProgress(newProgress: Double, currentLevel: Int, nextLevel: Int, isVisible : Boolean) { fun updateProgress(newProgress: Double, currentLevel: Int, nextLevel: Int, isVisible : Boolean) {
@ -50,4 +63,15 @@ class ProgressBar(
if(isVisible) if(isVisible)
repaint() 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,149 +1,324 @@
package KondoKit package KondoKit
import KondoKit.KondoKitUtils.convertValue 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.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import plugin.Plugin import plugin.Plugin
import plugin.PluginInfo
import plugin.PluginRepository import plugin.PluginRepository
import java.awt.* import java.awt.*
import java.lang.reflect.Field import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.* import java.util.*
import java.util.Timer import java.util.Timer
import javax.swing.* 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 { object ReflectiveEditorView {
fun createReflectiveEditorView(): JPanel { var reflectiveEditorView: JPanel? = null
private val loadedPlugins: MutableList<String> = mutableListOf()
const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW"
fun createReflectiveEditorView() {
val reflectiveEditorPanel = JPanel(BorderLayout()) val reflectiveEditorPanel = JPanel(BorderLayout())
reflectiveEditorPanel.background = VIEW_BACKGROUND_COLOR reflectiveEditorPanel.background = VIEW_BACKGROUND_COLOR
reflectiveEditorPanel.add(Box.createVerticalStrut(5)) reflectiveEditorPanel.add(Box.createVerticalStrut(5))
reflectiveEditorPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) reflectiveEditorPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
return reflectiveEditorPanel reflectiveEditorView = reflectiveEditorPanel
addPlugins(reflectiveEditorView!!)
} }
fun addPlugins(reflectiveEditorView: JPanel) { fun addPlugins(reflectiveEditorView: JPanel) {
reflectiveEditorView.removeAll() // clear previous
loadedPlugins.clear()
try { try {
val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins") val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins")
loadedPluginsField.isAccessible = true loadedPluginsField.isAccessible = true
val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *> val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *>
for ((_, plugin) in loadedPlugins) { for ((pluginInfo, plugin) in loadedPlugins) {
addPluginToEditor(reflectiveEditorView, plugin as Plugin) addPluginToEditor(reflectiveEditorView, pluginInfo as PluginInfo, plugin as Plugin)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
reflectiveEditorView.revalidate()
reflectiveEditorView.repaint()
}
private fun addPluginToEditor(reflectiveEditorView: JPanel, plugin: Any) { // Add a centered box for plugins that have no exposed fields
reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS) if (loadedPlugins.isNotEmpty()) {
val noExposedPanel = JPanel(BorderLayout())
noExposedPanel.background = VIEW_BACKGROUND_COLOR
noExposedPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
val fieldNotifier = KondoKitUtils.FieldNotifier(plugin) val label = JLabel("Loaded Plugins without Exposed Fields", SwingConstants.CENTER)
val exposedFields = KondoKitUtils.getKondoExposedFields(plugin) label.font = Font("RuneScape Small", Font.PLAIN, 16)
if (exposedFields.isNotEmpty()) {
val packageName = plugin.javaClass.`package`.name
val labelPanel = JPanel(BorderLayout())
labelPanel.maximumSize = Dimension(Int.MAX_VALUE, 30) // Adjust height to be minimal
labelPanel.background = VIEW_BACKGROUND_COLOR // Ensure it matches the overall background
labelPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
val label = JLabel("$packageName", SwingConstants.CENTER)
label.foreground = primaryColor label.foreground = primaryColor
label.font = Font("Arial", Font.BOLD, 14) noExposedPanel.add(label, BorderLayout.NORTH)
labelPanel.add(label, BorderLayout.CENTER)
reflectiveEditorView.add(labelPanel)
}
for (field in exposedFields) { val pluginsList = JList(loadedPlugins.toTypedArray())
field.isAccessible = true pluginsList.background = WIDGET_COLOR
pluginsList.foreground = secondaryColor
pluginsList.font = Font("RuneScape Small", Font.PLAIN, 16)
val fieldPanel = JPanel() // Wrap the JList in a JScrollPane with a fixed height
fieldPanel.layout = GridBagLayout() val maxScrollPaneHeight = 200
fieldPanel.background = WIDGET_COLOR // Match the background for minimal borders val scrollPane = JScrollPane(pluginsList).apply {
fieldPanel.foreground = secondaryColor verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) // No visible border, just spacing horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 40)
val gbc = GridBagConstraints()
gbc.insets = Insets(0, 5, 0, 5) // Less padding, more minimal spacing
val label = JLabel(field.name.removePrefix(KondoKitUtils.KONDO_PREFIX).capitalize())
label.foreground = secondaryColor
gbc.gridx = 0
gbc.gridy = 0
gbc.weightx = 0.0
gbc.anchor = GridBagConstraints.WEST
fieldPanel.add(label, gbc)
val textField = JTextField(field.get(plugin)?.toString() ?: "")
textField.background = VIEW_BACKGROUND_COLOR
textField.foreground = secondaryColor
textField.border = BorderFactory.createLineBorder(WIDGET_COLOR, 1) // Subtle border
gbc.gridx = 1
gbc.gridy = 0
gbc.weightx = 1.0
gbc.fill = GridBagConstraints.HORIZONTAL
fieldPanel.add(textField, gbc)
val applyButton = JButton("Apply")
applyButton.background = primaryColor
applyButton.foreground = VIEW_BACKGROUND_COLOR
applyButton.border = BorderFactory.createLineBorder(primaryColor, 1)
gbc.gridx = 2
gbc.gridy = 0
gbc.weightx = 0.0
gbc.fill = GridBagConstraints.NONE
applyButton.addActionListener {
try {
val newValue = convertValue(field.type, field.genericType, textField.text)
fieldNotifier.setFieldValue(field, newValue)
JOptionPane.showMessageDialog(
null,
"${field.name.removePrefix(KondoKitUtils.KONDO_PREFIX)} updated successfully!"
)
} catch (e: Exception) {
JOptionPane.showMessageDialog(
null,
"Failed to update ${field.name.removePrefix(KondoKitUtils.KONDO_PREFIX)}: ${e.message}",
"Error",
JOptionPane.ERROR_MESSAGE
)
}
} }
fieldPanel.add(applyButton, gbc) // Create a wrapper panel with BoxLayout to constrain the scroll pane
reflectiveEditorView.add(fieldPanel) val scrollPaneWrapper = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
add(scrollPane)
}
var previousValue = field.get(plugin)?.toString() noExposedPanel.add(scrollPaneWrapper, BorderLayout.CENTER)
val timer = Timer()
timer.schedule(object : TimerTask() { // Center the panel within the reflectiveEditorView
override fun run() { val centeredPanel = JPanel().apply {
val currentValue = field.get(plugin)?.toString() preferredSize = Dimension(240, maxScrollPaneHeight)
if (currentValue != previousValue) { maximumSize = preferredSize
previousValue = currentValue minimumSize = preferredSize
SwingUtilities.invokeLater { }
fieldNotifier.notifyFieldChange(field, currentValue) 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)
}, 0, 1000) }
fieldNotifier.addObserver(object : KondoKitUtils.FieldObserver { if (exposedFields.isNotEmpty()) {
override fun onFieldChange(field: Field, newValue: Any?) { reflectiveEditorView.add(Box.createVerticalStrut(5))
if (field.name.removePrefix(KondoKitUtils.KONDO_PREFIX).equals(label.text, ignoreCase = true)) { }
textField.text = newValue?.toString() ?: ""
textField.revalidate()
textField.repaint()
}
}
})
} }
if (exposedFields.isNotEmpty()) { else {
reflectiveEditorView.add(Box.createVerticalStrut(10)) 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

@ -0,0 +1,162 @@
package KondoKit
import KondoKit.plugin.Companion.SCROLL_BAR_COLOR
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import rt4.GameShell.frame
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Rectangle
import java.awt.event.*
import java.util.*
import javax.swing.JPanel
import javax.swing.SwingUtilities
class ScrollablePanel(private val content: JPanel) : JPanel() {
private var lastMouseY = 0
private var currentOffsetY = 0
private var scrollbarHeight = 0
private var scrollbarY = 0
private var showScrollbar = false
private var draggingScrollPill = false
private var lastSize = 0
// Define a buffer for the view height (extra space for smoother scrolling)
private val viewBuffer = -30
init {
layout = null
background = VIEW_BACKGROUND_COLOR // Color.red color can be set to debug
// Initial content bounds
content.bounds = Rectangle(0, 0, 242, content.preferredSize.height.coerceAtLeast(frame.height + viewBuffer))
add(content)
// Add listeners for scrolling interactions
addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
lastMouseY = e.y
if (showScrollbar && e.x in (242 - 10)..242 && e.y in scrollbarY..(scrollbarY + scrollbarHeight)) {
draggingScrollPill = true
}
}
override fun mouseReleased(e: MouseEvent) {
draggingScrollPill = false
}
})
addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseDragged(e: MouseEvent) {
val deltaY = e.y - lastMouseY
if (draggingScrollPill && showScrollbar) {
val viewHeight = frame.height
val contentHeight = content.height
val scrollRatio = contentHeight.toDouble() / viewHeight
scrollContent((deltaY * scrollRatio).toInt())
} else if (showScrollbar) {
scrollContent(deltaY)
}
lastMouseY = e.y
}
})
addMouseWheelListener { e ->
if (showScrollbar) {
scrollContent(-e.wheelRotation * 20)
}
}
// Timer to periodically check and update scrollbar status
Timer().schedule(object : TimerTask() {
override fun run() {
updateScrollbar()
if(lastSize != content.preferredSize.height.coerceAtLeast(frame.height + viewBuffer))
handleResize()
}
}, 0, 1000)
// Component listener for resizing the frame
frame.addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
handleResize()
}
})
}
private fun handleResize() {
SwingUtilities.invokeLater{
// Ensure the ScrollablePanel resizes with the frame
bounds = Rectangle(0, 0, 242, frame.height)
// Dynamically update content bounds and scrollbar on frame resize with buffer
lastSize = content.preferredSize.height.coerceAtLeast(frame.height + viewBuffer)
content.bounds = Rectangle(0, 0, 242, lastSize)
showScrollbar = content.height > frame.height
currentOffsetY = 0
content.setLocation(0, currentOffsetY)
updateScrollbar()
revalidate()
repaint()
}
}
private fun scrollContent(deltaY: Int) {
if (!showScrollbar) {
currentOffsetY = 0
content.setLocation(0, currentOffsetY)
return
}
SwingUtilities.invokeLater {
currentOffsetY += deltaY
// Apply buffer to maxOffset
val maxOffset = (frame.height - content.height + viewBuffer).coerceAtMost(0)
currentOffsetY = currentOffsetY.coerceAtMost(0).coerceAtLeast(maxOffset)
content.setLocation(0, currentOffsetY)
val contentHeight = content.height
val viewHeight = frame.height + viewBuffer
val scrollableRatio = viewHeight.toDouble() / contentHeight
scrollbarY = ((-currentOffsetY / contentHeight.toDouble()) * viewHeight).toInt()
scrollbarHeight = (viewHeight * scrollableRatio).toInt().coerceAtLeast(20)
repaint()
}
}
private fun updateScrollbar() {
SwingUtilities.invokeLater {
showScrollbar = content.height > frame.height
val contentHeight = content.height
val viewHeight = frame.height + viewBuffer
if (showScrollbar) {
val scrollableRatio = viewHeight.toDouble() / contentHeight
scrollbarY = ((-currentOffsetY / contentHeight.toDouble()) * viewHeight).toInt()
scrollbarHeight = (viewHeight * scrollableRatio).toInt().coerceAtLeast(20)
} else {
scrollbarY = 0
scrollbarHeight = 0
}
repaint()
}
}
override fun paintComponent(g: Graphics) {
super.paintComponent(g)
}
override fun paintChildren(g: Graphics) {
super.paintChildren(g)
if (showScrollbar) {
val g2 = g as Graphics2D
val scrollbarX = 238
g2.color = SCROLL_BAR_COLOR
g2.fillRect(scrollbarX, scrollbarY, 2, scrollbarHeight)
}
}
}

View file

@ -4,109 +4,196 @@ import rt4.GlIndexedSprite
import rt4.GlSprite import rt4.GlSprite
import rt4.SoftwareIndexedSprite import rt4.SoftwareIndexedSprite
import rt4.SoftwareSprite import rt4.SoftwareSprite
import java.awt.Color
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
// Define interfaces for common sprite types
interface BaseSprite {
val width: Int
val height: Int
}
interface IndexedSprite : BaseSprite {
val pixels: ByteArray
val palette: IntArray
}
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
override val height: Int = sprite.height
override val pixels: IntArray? = sprite.pixels
}
}
fun adaptSoftwareIndexedSprite(sprite: SoftwareIndexedSprite): IndexedSprite {
return object : IndexedSprite {
override val width: Int = sprite.width
override val height: Int = sprite.height
override val pixels: ByteArray = sprite.pixels
override val palette: IntArray = sprite.pallet
}
}
fun adaptGlSprite(sprite: GlSprite): NonIndexedSprite {
return object : NonIndexedSprite {
override val width: Int = sprite.width
override val height: Int = sprite.height
override val pixels: IntArray? = sprite.pixels
}
}
fun adaptGlIndexedSprite(sprite: GlIndexedSprite): IndexedSprite {
return object : IndexedSprite {
override val width: Int = sprite.width
override val height: Int = sprite.height
override val pixels: ByteArray = sprite.pixels
override val palette: IntArray = sprite.pallet
}
}
object SpriteToBufferedImage { object SpriteToBufferedImage {
/** /**
* Converts a SoftwareSprite back into a BufferedImage. * Converts a BaseSprite into a BufferedImage.
*
* Handles both indexed and non-indexed sprites, with optional tinting and grayscale.
* *
* @param sprite The sprite to be converted. * @param sprite The sprite to be converted.
* @param tint An optional Color to tint the image.
* @param grayscale If true, converts the image to grayscale.
* @param brightnessBoost A multiplier to boost the brightness of the image.
* @return The BufferedImage created from the sprite. * @return The BufferedImage created from the sprite.
*/ */
fun convertToBufferedImage(sprite: SoftwareSprite): BufferedImage { private fun convertToBufferedImage(
sprite: BaseSprite,
tint: Color? = null,
grayscale: Boolean = false,
brightnessBoost: Float = 1.0f
): BufferedImage {
val width = sprite.width val width = sprite.width
val height = sprite.height val height = sprite.height
val pixels = sprite.pixels
// Create a BufferedImage with ARGB color model
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
// Manually set pixels and print the pixel data when (sprite) {
for (y in 0 until height) { is IndexedSprite -> {
for (x in 0 until width) { val pixels = sprite.pixels
val pixel = pixels[y * width + x] val palette = sprite.palette
image.setRGB(x, y, pixel)
// Manually set pixels using the palette
for (y in 0 until height) {
for (x in 0 until width) {
val index = pixels[y * width + x].toInt() and 0xFF
val color = palette[index]
// Apply grayscale or tint if provided
val finalColor = if (grayscale) {
applyGrayscale(Color(color, true), brightnessBoost)
} else if (tint != null) {
applyTint(Color(color, true), tint, brightnessBoost)
} else {
applyBrightness(Color(color, true), brightnessBoost)
}
image.setRGB(x, y, finalColor.rgb)
}
}
}
is NonIndexedSprite -> {
val pixels = sprite.pixels ?: return image // Handle null case for GlSprite
// Manually set pixels directly
for (y in 0 until height) {
for (x in 0 until width) {
val color = pixels[y * width + x]
// Apply grayscale or tint if provided
val finalColor = if (grayscale) {
applyGrayscale(Color(color, true), brightnessBoost)
} else if (tint != null) {
applyTint(Color(color, true), tint, brightnessBoost)
} else {
applyBrightness(Color(color, true), brightnessBoost)
}
image.setRGB(x, y, finalColor.rgb)
}
}
} }
} }
return image return image
} }
fun convertToBufferedImage(sprite: SoftwareIndexedSprite): BufferedImage { /**
val width = sprite.width * Applies a tint to a given color using the tint's alpha value to control the intensity.
val height = sprite.height *
val pixels = sprite.pixels // byte[] * @param original The original color.
val palette = sprite.pallet * @param tint The tint color to be applied.
* @param brightnessBoost A multiplier to boost the brightness of the image.
// Create a BufferedImage with ARGB color model * @return The tinted color.
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) */
private fun applyTint(original: Color, tint: Color, brightnessBoost: Float): Color {
// Manually set pixels using the palette val boostedColor = applyBrightness(original, brightnessBoost)
for (y in 0 until height) { val r = (boostedColor.red * tint.red / 255).coerceIn(0, 255)
for (x in 0 until width) { val g = (boostedColor.green * tint.green / 255).coerceIn(0, 255)
// Get the index from the sprite's pixel array val b = (boostedColor.blue * tint.blue / 255).coerceIn(0, 255)
val index = pixels[y * width + x].toInt() and 0xFF return Color(r, g, b, boostedColor.alpha)
// Map the index to a color in the palette
val color = palette[index]
// Set the ARGB color in the BufferedImage
image.setRGB(x, y, color)
}
}
return image
} }
fun convertToBufferedImage(sprite: GlIndexedSprite): BufferedImage { /**
val width = sprite.width * Boosts the brightness of a given color.
val height = sprite.height *
val pixels = sprite.pixels // byte[] * @param original The original color.
val palette = sprite.pallet * @param factor The multiplier to boost the brightness.
* @return The color with boosted brightness.
// Create a BufferedImage with ARGB color model */
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) private fun applyBrightness(original: Color, factor: Float): Color {
val r = (original.red * factor).coerceIn(0.0f, 255.0f).toInt()
// Manually set pixels using the palette val g = (original.green * factor).coerceIn(0.0f, 255.0f).toInt()
for (y in 0 until height) { val b = (original.blue * factor).coerceIn(0.0f, 255.0f).toInt()
for (x in 0 until width) { return Color(r, g, b, original.alpha)
// Get the index from the sprite's pixel array
val index = pixels[y * width + x].toInt() and 0xFF
// Map the index to a color in the palette
val color = palette[index]
// Set the ARGB color in the BufferedImage
image.setRGB(x, y, color)
}
}
return image
} }
/**
fun convertToBufferedImage(sprite: GlSprite): BufferedImage { * Converts a color to grayscale and applies a brightness boost.
val width = sprite.width *
val height = sprite.height * @param original The original color.
val pixels = sprite.pixels * @param brightnessBoost A multiplier to boost the brightness.
* @return The grayscale version of the color with boosted brightness.
// Create a BufferedImage with ARGB color model */
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) private fun applyGrayscale(original: Color, brightnessBoost: Float): Color {
// Calculate the grayscale value using the luminosity method
if(pixels == null) { val grayValue = (0.3 * original.red + 0.59 * original.green + 0.11 * original.blue).toInt()
return image val boostedGray = (grayValue * brightnessBoost).coerceIn(0.0f, 255.0f).toInt()
} return Color(boostedGray, boostedGray, boostedGray, original.alpha)
// Manually set pixels and print the pixel data
for (y in 0 until height) {
for (x in 0 until width) {
val pixel = pixels[y * width + x]
image.setRGB(x, y, pixel)
}
}
return image
} }
fun getBufferedImageFromSprite(sprite: Any?) : BufferedImage { /**
return when(sprite){ * Converts an unknown sprite object into a BufferedImage if it matches a known sprite type.
is GlSprite -> convertToBufferedImage(sprite) *
is SoftwareSprite -> convertToBufferedImage(sprite) * @param sprite The sprite object to be converted.
is SoftwareIndexedSprite -> convertToBufferedImage(sprite) * @param tint An optional Color to tint the image.
is GlIndexedSprite -> convertToBufferedImage(sprite) * @param grayscale If true, converts the image to grayscale.
else -> BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) * @param brightnessBoost A multiplier to boost the brightness of the image.
* @return The BufferedImage created from the sprite or a default image if unsupported.
*/
fun getBufferedImageFromSprite(
sprite: Any?,
tint: Color? = null,
grayscale: Boolean = false,
brightnessBoost: Float = 1.0f
): BufferedImage {
return when (sprite) {
is SoftwareSprite -> convertToBufferedImage(adaptSoftwareSprite(sprite), tint, grayscale, brightnessBoost)
is SoftwareIndexedSprite -> convertToBufferedImage(adaptSoftwareIndexedSprite(sprite), tint, grayscale, brightnessBoost)
is GlSprite -> convertToBufferedImage(adaptGlSprite(sprite), tint, grayscale, brightnessBoost)
is GlIndexedSprite -> convertToBufferedImage(adaptGlIndexedSprite(sprite), tint, grayscale, brightnessBoost)
else -> BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) // Default empty image for unsupported types
} }
} }
} }

View file

@ -0,0 +1,94 @@
package KondoKit
import java.awt.Color
object Themes {
enum class ThemeType {
RUNELITE,
DARKER_RUNELITE,
SARADOMIN,
ORION,
}
fun getTheme(themeType: ThemeType): Theme {
return when (themeType) {
ThemeType.RUNELITE -> Theme(
widgetColor = Color(30, 30, 30),
titleBarColor = Color(21, 21, 21),
viewBackgroundColor = Color(40, 40, 40),
primaryColor = Color(165, 165, 165),
secondaryColor = Color(255, 255, 255),
popupBackground = Color(45, 45, 45),
popupForeground = Color(220, 220, 220),
tooltipBackground = Color(50, 50, 50),
scrollBarColor = Color(64, 64, 64),
progressBarFill = Color(61, 56, 49),
navTint = null,
navGreyScale = false,
boost = 1f
)
ThemeType.DARKER_RUNELITE -> Theme(
widgetColor = Color(15, 15, 15),
titleBarColor = Color(10, 10, 10),
viewBackgroundColor = Color(20, 20, 20),
primaryColor = Color(140, 140, 140),
secondaryColor = Color(210, 210, 210),
popupBackground = Color(25, 25, 25),
popupForeground = Color(200, 200, 200),
tooltipBackground = Color(30, 30, 30),
scrollBarColor = Color(40, 40, 40),
progressBarFill = Color(45, 40, 35),
navTint = null,
navGreyScale = false,
boost = 0.8f
)
ThemeType.ORION -> Theme(
widgetColor = Color(50, 50, 50),
titleBarColor = Color(35, 35, 35),
viewBackgroundColor = Color(60, 60, 60),
primaryColor = Color(180, 180, 180),
secondaryColor = Color(210, 210, 210),
popupBackground = Color(45, 45, 45),
popupForeground = Color(230, 230, 230),
tooltipBackground = Color(55, 55, 55),
scrollBarColor = Color(75, 75, 75),
progressBarFill = Color(100, 100, 100),
navTint = null,
navGreyScale = true,
boost = 1.3f
)
ThemeType.SARADOMIN -> Theme(
widgetColor = Color(62, 53, 41),
titleBarColor = Color(111, 93, 69).darker(),
viewBackgroundColor = Color(101, 85, 63),
primaryColor = Color(180, 150, 120),
secondaryColor = Color(230, 210, 190),
popupBackground = Color(70, 56, 42),
popupForeground = Color(220, 200, 180),
tooltipBackground = Color(80, 65, 50),
scrollBarColor = Color(100, 85, 70),
progressBarFill = Color(130, 110, 90),
navTint = null,
navGreyScale = false,
boost = 1f
)
}
}
data class Theme(
val widgetColor: Color,
val titleBarColor: Color,
val viewBackgroundColor: Color,
val primaryColor: Color,
val secondaryColor: Color,
val popupBackground: Color,
val popupForeground: Color,
val tooltipBackground: Color,
val scrollBarColor: Color,
val progressBarFill: Color,
val navTint: Color?,
val navGreyScale: Boolean,
val boost: Float
)
}

View file

@ -6,9 +6,9 @@ import rt4.Node
object XPTable { object XPTable {
const val MAX_LEVEL = 99 private const val MAX_LEVEL = 99
const val INVALID_LEVEL = -1 private const val INVALID_LEVEL = -1
const val SKILLS_XP_TABLE = 716 private const val SKILLS_XP_TABLE = 716
private var xpTable: MutableList<Int> = mutableListOf() private var xpTable: MutableList<Int> = mutableListOf()

View file

@ -1,35 +1,43 @@
package KondoKit package KondoKit
import KondoKit.Helpers.addMouseListenerToAll
import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.Helpers.formatNumber import KondoKit.Helpers.formatNumber
import KondoKit.Helpers.getProgressBarColor import KondoKit.Helpers.getProgressBarColor
import KondoKit.Helpers.getSpriteId import KondoKit.Helpers.getSpriteId
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.plugin.Companion.IMAGE_SIZE 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.TOTAL_XP_WIDGET_SIZE
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.WIDGET_SIZE import KondoKit.plugin.Companion.WIDGET_SIZE
import KondoKit.plugin.Companion.kondoExposed_playerXPMultiplier import KondoKit.plugin.Companion.playerXPMultiplier
import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.totalXPWidget import KondoKit.plugin.StateManager.focusedView
import plugin.api.API import plugin.api.API
import java.awt.* import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.swing.Box import javax.swing.*
import javax.swing.BoxLayout
import javax.swing.JLabel
import javax.swing.JPanel
object XPTrackerView { object XPTrackerView {
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4) 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 { val npcHitpointsMap: Map<Int, Int> = try {
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("npc_hitpoints_map.json"), StandardCharsets.UTF_8)) BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/npc_hitpoints_map.json"), StandardCharsets.UTF_8))
.useLines { lines -> .useLines { lines ->
val json = lines.joinToString("\n") val json = lines.joinToString("\n")
val pairs = json.trim().removeSurrounding("{", "}").split(",") val pairs = json.trim().removeSurrounding("{", "}").split(",")
@ -74,8 +82,8 @@ object XPTrackerView {
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) { if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId] val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
val xpPerKill = when (xpWidget.skillId) { val xpPerKill = when (xpWidget.skillId) {
3 -> kondoExposed_playerXPMultiplier * (npcHP ?: 1) // Hitpoints 3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints
else -> kondoExposed_playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
} }
val remainingKills = xpLeft / xpPerKill val remainingKills = xpLeft / xpPerKill
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor) xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
@ -92,24 +100,61 @@ object XPTrackerView {
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor) xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
// Update the progress bar with current level, progress, and next level // Update the progress bar with current level, progress, and next level
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, plugin.StateManager.focusedView == "XP_TRACKER_VIEW") xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME)
xpWidget.previousXp = xp xpWidget.previousXp = xp
if (plugin.StateManager.focusedView == "XP_TRACKER_VIEW") if (focusedView == VIEW_NAME)
xpWidget.panel.repaint() xpWidget.container.repaint()
} }
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) { private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
val totalXPWidget = plugin.StateManager.totalXPWidget ?: return val totalXPWidget = totalXPWidget ?: return
totalXPWidget.totalXpGained += xpGainedSinceLastUpdate totalXPWidget.totalXpGained += xpGainedSinceLastUpdate
val formattedXp = formatNumber(totalXPWidget.totalXpGained) val formattedXp = formatNumber(totalXPWidget.totalXpGained)
totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor) totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor)
if (plugin.StateManager.focusedView == "XP_TRACKER_VIEW") if (focusedView == VIEW_NAME)
totalXPWidget.panel.repaint() 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 { fun createTotalXPWidget(): XPWidget {
val widgetPanel = Panel().apply { val widgetPanel = Panel().apply {
@ -120,43 +165,39 @@ object XPTrackerView {
minimumSize = TOTAL_XP_WIDGET_SIZE minimumSize = TOTAL_XP_WIDGET_SIZE
} }
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(898)) val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(LVL_ICON))
val imageContainer = Panel(FlowLayout()).apply { val imageContainer = Panel(FlowLayout()).apply {
background = WIDGET_COLOR preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
preferredSize = IMAGE_SIZE maximumSize = preferredSize
maximumSize = IMAGE_SIZE minimumSize = preferredSize
minimumSize = IMAGE_SIZE size = preferredSize
size = IMAGE_SIZE
} }
bufferedImageSprite.let { image -> bufferedImageSprite.let { image ->
val imageCanvas = ImageCanvas(image).apply { val imageCanvas = ImageCanvas(image).apply {
background = WIDGET_COLOR preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
preferredSize = Dimension(image.width, image.height) maximumSize = preferredSize
maximumSize = Dimension(image.width, image.height) minimumSize = preferredSize
minimumSize = Dimension(image.width, image.height) size = preferredSize
size = Dimension(image.width, image.height)
} }
imageContainer.add(imageCanvas) imageContainer.add(imageCanvas)
imageContainer.size = Dimension(image.width, image.height) imageContainer.size = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
imageContainer.revalidate() imageContainer.revalidate()
if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW") if(focusedView == VIEW_NAME)
imageContainer.repaint() imageContainer.repaint()
} }
val textPanel = Panel().apply { val textPanel = Panel().apply {
layout = GridLayout(2, 1, 5, 5) layout = GridLayout(2, 1, 5, 0)
background = WIDGET_COLOR
} }
val font = Font("Arial", Font.PLAIN, 11) val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val xpGainedLabel = JLabel( val xpGainedLabel = JLabel(
formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor) formatHtmlLabelText("Gained: ", primaryColor, "0", secondaryColor)
).apply { ).apply {
this.horizontalAlignment = JLabel.LEFT this.horizontalAlignment = JLabel.LEFT
this.font = font this.font = font
@ -177,14 +218,14 @@ object XPTrackerView {
return XPWidget( return XPWidget(
skillId = -1, skillId = -1,
panel = widgetPanel, container = widgetPanel,
xpGainedLabel = xpGainedLabel, xpGainedLabel = xpGainedLabel,
xpLeftLabel = JLabel(formatHtmlLabelText("XP Left: ", primaryColor, "0", secondaryColor)).apply { xpLeftLabel = JLabel(formatHtmlLabelText("XP Left: ", primaryColor, "0", secondaryColor)).apply {
this.horizontalAlignment = JLabel.LEFT this.horizontalAlignment = JLabel.LEFT
this.font = font this.font = font
}, },
xpPerHourLabel = xpPerHourLabel, xpPerHourLabel = xpPerHourLabel,
progressBar = ProgressBar(0.0, Color(150, 50, 50)), progressBar = ProgressBar(0.0, Color.BLACK), // Unused
totalXpGained = 0, totalXpGained = 0,
startTime = System.currentTimeMillis(), startTime = System.currentTimeMillis(),
previousXp = 0, previousXp = 0,
@ -193,19 +234,66 @@ object XPTrackerView {
} }
fun createXPTrackerView(): JPanel? { fun createXPTrackerView(){
val widgetViewPanel = JPanel() val widgetViewPanel = JPanel().apply {
widgetViewPanel.layout = BoxLayout(widgetViewPanel, BoxLayout.Y_AXIS) layout = BoxLayout(this, BoxLayout.Y_AXIS)
widgetViewPanel.background = VIEW_BACKGROUND_COLOR background = VIEW_BACKGROUND_COLOR
widgetViewPanel.add(Box.createVerticalStrut(5)) }
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() totalXPWidget = createTotalXPWidget()
widgetViewPanel.add(wrappedWidget(totalXPWidget!!.panel)) val wrapped = wrappedWidget(totalXPWidget!!.container)
addMouseListenerToAll(wrapped,rightClickListener)
wrapped.addMouseListener(rightClickListener)
widgetViewPanel.add(Box.createVerticalStrut(5))
widgetViewPanel.add(wrapped)
widgetViewPanel.add(Box.createVerticalStrut(5)) widgetViewPanel.add(Box.createVerticalStrut(5))
return widgetViewPanel 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 { fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
val widgetPanel = Panel().apply { val widgetPanel = Panel().apply {
layout = BorderLayout(5, 5) layout = BorderLayout(5, 5)
@ -237,19 +325,19 @@ object XPTrackerView {
imageContainer.size = Dimension(image.width, image.height) // Ensure container respects the image size imageContainer.size = Dimension(image.width, image.height) // Ensure container respects the image size
imageContainer.revalidate() imageContainer.revalidate()
if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW") if(focusedView == VIEW_NAME)
imageContainer.repaint() imageContainer.repaint()
} }
val textPanel = Panel().apply { val textPanel = Panel().apply {
layout = GridLayout(2, 2, 5, 5) layout = GridLayout(2, 2, 5, 0)
background = WIDGET_COLOR background = WIDGET_COLOR
} }
val font = Font("Arial", Font.PLAIN, 11) val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
val xpGainedLabel = JLabel( val xpGainedLabel = JLabel(
formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor) formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor)
).apply { ).apply {
this.horizontalAlignment = JLabel.LEFT this.horizontalAlignment = JLabel.LEFT
this.font = font this.font = font
@ -278,7 +366,7 @@ object XPTrackerView {
val levelPanel = Panel().apply { val levelPanel = Panel().apply {
layout = BorderLayout(5, 0) layout = BorderLayout(5, 0)
background = Color(43, 43, 43) background = WIDGET_COLOR
} }
val progressBarPanel = ProgressBar(0.0, getProgressBarColor(skillId)).apply { val progressBarPanel = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
@ -297,12 +385,12 @@ object XPTrackerView {
widgetPanel.add(levelPanel, BorderLayout.SOUTH) widgetPanel.add(levelPanel, BorderLayout.SOUTH)
widgetPanel.revalidate() widgetPanel.revalidate()
if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW") if(focusedView == VIEW_NAME)
widgetPanel.repaint() widgetPanel.repaint()
return XPWidget( return XPWidget(
skillId = skillId, skillId = skillId,
panel = widgetPanel, container = widgetPanel,
xpGainedLabel = xpGainedLabel, xpGainedLabel = xpGainedLabel,
xpLeftLabel = xpLeftLabel, xpLeftLabel = xpLeftLabel,
xpPerHourLabel = xpPerHourLabel, xpPerHourLabel = xpPerHourLabel,
@ -314,18 +402,18 @@ object XPTrackerView {
) )
} }
fun wrappedWidget(component: Component, padding: Int = 7): Panel { fun wrappedWidget(component: Component, padding: Int = 7): Container {
val outerPanelSize = Dimension( val outerPanelSize = Dimension(
component.preferredSize.width + 2 * padding, component.preferredSize.width + 2 * padding,
component.preferredSize.height + 2 * padding component.preferredSize.height + 2 * padding
) )
val outerPanel = Panel(GridBagLayout()).apply { val outerPanel = JPanel(GridBagLayout()).apply {
background = WIDGET_COLOR background = WIDGET_COLOR
preferredSize = outerPanelSize preferredSize = outerPanelSize
maximumSize = outerPanelSize maximumSize = outerPanelSize
minimumSize = outerPanelSize minimumSize = outerPanelSize
} }
val innerPanel = Panel(BorderLayout()).apply { val innerPanel = JPanel(BorderLayout()).apply {
background = WIDGET_COLOR background = WIDGET_COLOR
preferredSize = component.preferredSize preferredSize = component.preferredSize
maximumSize = component.preferredSize maximumSize = component.preferredSize
@ -343,14 +431,14 @@ object XPTrackerView {
data class XPWidget( data class XPWidget(
val panel: Panel, val container: Container,
val skillId: Int, val skillId: Int,
val xpGainedLabel: JLabel, val xpGainedLabel: JLabel,
val xpLeftLabel: JLabel, val xpLeftLabel: JLabel,
val xpPerHourLabel: JLabel, val xpPerHourLabel: JLabel,
val actionsRemainingLabel: JLabel, val actionsRemainingLabel: JLabel,
val progressBar: ProgressBar, val progressBar: ProgressBar,
var totalXpGained: Int = 0, var totalXpGained: Int = 0,
var startTime: Long = System.currentTimeMillis(), var startTime: Long = System.currentTimeMillis(),
var previousXp: Int = 0 var previousXp: Int = 0
) )

View file

@ -4,214 +4,201 @@ import KondoKit.Constants.COMBAT_LVL_SPRITE
import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.Helpers.formatNumber import KondoKit.Helpers.formatNumber
import KondoKit.Helpers.getSpriteId import KondoKit.Helpers.getSpriteId
import KondoKit.Helpers.showAlert
import KondoKit.HiscoresView.createHiscoreSearchView import KondoKit.HiscoresView.createHiscoreSearchView
import KondoKit.HiscoresView.hiScoreView
import KondoKit.LootTrackerView.BAG_ICON import KondoKit.LootTrackerView.BAG_ICON
import KondoKit.LootTrackerView.createLootTrackerView import KondoKit.LootTrackerView.createLootTrackerView
import KondoKit.LootTrackerView.lootTrackerView
import KondoKit.LootTrackerView.npcDeathSnapshots import KondoKit.LootTrackerView.npcDeathSnapshots
import KondoKit.LootTrackerView.onPostClientTick import KondoKit.LootTrackerView.onPostClientTick
import KondoKit.LootTrackerView.takeGroundSnapshot import KondoKit.LootTrackerView.takeGroundSnapshot
import KondoKit.ReflectiveEditorView.addPlugins import KondoKit.ReflectiveEditorView.addPlugins
import KondoKit.ReflectiveEditorView.createReflectiveEditorView import KondoKit.ReflectiveEditorView.createReflectiveEditorView
import KondoKit.ReflectiveEditorView.reflectiveEditorView
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.XPTrackerView.createTotalXPWidget import KondoKit.Themes.Theme
import KondoKit.Themes.ThemeType
import KondoKit.Themes.getTheme
import KondoKit.XPTrackerView.createXPTrackerView import KondoKit.XPTrackerView.createXPTrackerView
import KondoKit.XPTrackerView.createXPWidget import KondoKit.XPTrackerView.createXPWidget
import KondoKit.XPTrackerView.initialXP
import KondoKit.XPTrackerView.resetXPTracker
import KondoKit.XPTrackerView.totalXPWidget
import KondoKit.XPTrackerView.updateWidget import KondoKit.XPTrackerView.updateWidget
import KondoKit.XPTrackerView.wrappedWidget import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.plugin.StateManager.initialXP import KondoKit.XPTrackerView.xpTrackerView
import KondoKit.plugin.StateManager.totalXPWidget import KondoKit.XPTrackerView.xpWidgets
import KondoKit.plugin.StateManager.xpWidgets import KondoKit.plugin.StateManager.focusedView
import plugin.Plugin import plugin.Plugin
import plugin.api.* import plugin.api.*
import plugin.api.API.* import plugin.api.API.*
import plugin.api.FontColor.fromColor import plugin.api.FontColor.fromColor
import rt4.GameShell import rt4.*
import rt4.DisplayMode
import rt4.GameShell.canvas
import rt4.GameShell.frame import rt4.GameShell.frame
import rt4.GlRenderer
import rt4.InterfaceList
import rt4.Player
import rt4.client.js5Archive8 import rt4.client.js5Archive8
import rt4.client.mainLoadState import rt4.client.mainLoadState
import java.awt.* import java.awt.*
import java.awt.Font
import java.awt.event.ActionListener import java.awt.event.ActionListener
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.* import javax.swing.*
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Exposed(val description: String = "")
class plugin : Plugin() { class plugin : Plugin() {
companion object { companion object {
val WIDGET_SIZE = Dimension(270, 55) val WIDGET_SIZE = Dimension(220, 50)
val TOTAL_XP_WIDGET_SIZE = Dimension(270, 30) val TOTAL_XP_WIDGET_SIZE = Dimension(220, 30)
val IMAGE_SIZE = Dimension(20, 20) val IMAGE_SIZE = Dimension(25, 23)
val WIDGET_COLOR = Color(27, 27, 27)
val VIEW_BACKGROUND_COLOR = Color(37, 37, 37) // Default Theme Colors
val primaryColor = Color(129, 129, 129) // Color for "XP Gained:" var WIDGET_COLOR = Color(30, 30, 30)
val secondaryColor = Color(226, 226, 226) // Color for "0" var TITLE_BAR_COLOR = Color(21, 21, 21)
var kondoExposed_useLiveGEPrices = true var VIEW_BACKGROUND_COLOR = Color(40, 40, 40)
var kondoExposed_playerXPMultiplier = 5 var primaryColor = Color(165, 165, 165) // Color for "XP Gained:"
const val FIXED_WIDTH = 782 var secondaryColor = Color(255, 255, 255) // Color for "0"
const val SCROLLPANE_WIDTH = 340 var POPUP_BACKGROUND = Color(45, 45, 45)
var POPUP_FOREGROUND = Color(220, 220, 220)
var TOOLTIP_BACKGROUND = Color(50,50,50)
var SCROLL_BAR_COLOR = Color(64, 64, 64)
var PROGRESS_BAR_FILL = Color(61, 56, 49)
var NAV_TINT: Color? = null
var NAV_GREYSCALE = false
var BOOST = 1f
var appliedTheme = ThemeType.RUNELITE
@Exposed("Theme colors for KondoKit, requires a relaunch to apply.")
var theme = ThemeType.RUNELITE
@Exposed("Default: true, Use Local JSON or the prices from the Live/Stable server API")
var useLiveGEPrices = true
@Exposed("Used to calculate Combat Actions until next level.")
var playerXPMultiplier = 5
@Exposed("Start minimized/collapsed by default")
var launchMinimized = false
@Exposed("Default 16 on Windows, 0 Linux/macOS. If Kondo is not " +
"perfectly snapped to the edge of the game due to window chrome you can update this to fix it")
var uiOffset = 0
@Exposed("Stretched/Scaled Fixed Mode Support")
var useScaledFixed = false
const val FIXED_WIDTH = 765
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 WRENCH_ICON = 907
private const val LVL_ICON = 898
private const val LOOT_ICON = 777 private const val LOOT_ICON = 777
private const val MAG_SPRITE = 1423 private const val MAG_SPRITE = 1423
const val LVL_ICON = 898
private lateinit var cardLayout: CardLayout private lateinit var cardLayout: CardLayout
private lateinit var mainContentPanel: Panel private lateinit var mainContentPanel: JPanel
private var scrollPane: JScrollPane? = null private var rightPanelWrapper: JScrollPane? = null
private var hiScoreView: JPanel? = null
private var reflectiveEditorView: JPanel? = null
private var lootTrackerView: JPanel? = null
private var xpTrackerView: JPanel? = null
private var accumulatedTime = 0L private var accumulatedTime = 0L
private const val tickInterval = 600L private var reloadInterfaces = false
private const val TICK_INTERVAL = 600L
private var pluginsReloaded = false private var pluginsReloaded = false
private var loginScreen = 160; private var loginScreen = 160
private var lastLogin = "" private var lastLogin = ""
private var initialized = false; private var initialized = false
private var lastClickTime = 0L
private var lastUIOffset = 0
private var themeName = "RUNELITE"
private const val HIDDEN_VIEW = "HIDDEN"
private var altCanvas: AltCanvas? = null
private val drawActions = mutableListOf<() -> Unit>()
fun registerDrawAction(action: () -> Unit) {
synchronized(drawActions) {
drawActions.add(action)
}
}
} }
fun allSpritesLoaded() : Boolean { override fun Init() {
// Check all skill sprites // Disable Font AA
try{ System.setProperty("sun.java2d.opengl", "false")
for (i in 0 until 24) { System.setProperty("awt.useSystemAAFontSettings", "off")
if(!js5Archive8.isFileReady(getSpriteId(i))){ System.setProperty("swing.aatext", "false")
return false;
}
}
val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, COMBAT_LVL_SPRITE, BAG_ICON);
for (icon in otherIcons) {
if(!js5Archive8.isFileReady(icon)){
return false;
}
}
} catch (e : Exception){
return false;
}
return true;
} }
override fun OnLogin() { override fun OnLogin() {
if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) { if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) {
// if we logged in with a new character // if we logged in with a new character
// we need to reset the trackers // we need to reset the trackers
xpTrackerView?.removeAll() xpTrackerView?.let { resetXPTracker(it) }
totalXPWidget = createTotalXPWidget()
xpTrackerView?.add(Box.createVerticalStrut(5))
xpTrackerView?.add(wrappedWidget(totalXPWidget!!.panel))
xpTrackerView?.add(Box.createVerticalStrut(5))
initialXP.clear()
xpWidgets.clear()
xpTrackerView?.revalidate()
if (StateManager.focusedView == "XP_TRACKER_VIEW")
xpTrackerView?.repaint()
} }
lastLogin = Player.usernameInput.toString() lastLogin = Player.usernameInput.toString()
} }
private fun UpdateDisplaySettings() {
val mode = GetWindowMode()
when (mode) {
WindowMode.FIXED -> {
if (frame.width < FIXED_WIDTH + SCROLLPANE_WIDTH) {
frame.setSize(FIXED_WIDTH + SCROLLPANE_WIDTH, frame.height)
}
val difference = frame.width - (FIXED_WIDTH + SCROLLPANE_WIDTH)
GameShell.leftMargin = difference / 2
}
WindowMode.RESIZABLE -> {
GameShell.canvasWidth -= SCROLLPANE_WIDTH
}
}
scrollPane?.revalidate()
scrollPane?.repaint()
}
fun OnKondoValueUpdated(){
StoreData("kondoUseRemoteGE", kondoExposed_useLiveGEPrices)
StoreData("kondoPlayerXPMultiplier", kondoExposed_playerXPMultiplier)
LootTrackerView.gePriceMap = LootTrackerView.loadGEPrices()
}
override fun OnMiniMenuCreate(currentEntries: Array<out MiniMenuEntry>?) { override fun OnMiniMenuCreate(currentEntries: Array<out MiniMenuEntry>?) {
if (currentEntries != null) { if (currentEntries != null) {
for ((index, entry) in currentEntries.withIndex()) { for ((index, entry) in currentEntries.withIndex()) {
if (entry.type == MiniMenuType.PLAYER && index == currentEntries.size - 1) { if (entry.type == MiniMenuType.PLAYER && index == currentEntries.size - 1) {
val input = entry.subject val input = entry.subject
val username = input // Trim spaces, clean up tags, and remove the level info
.replace(Regex("<col=[0-9a-fA-F]{6}>"), "") val cleanedInput = input
.replace(Regex("<img=\\d+>"), "") .trim() // Remove any leading/trailing spaces
.split(" ") // Split by spaces .replace(Regex("<col=[0-9a-fA-F]{6}>"), "") // Remove color tags
.first() // Take the first part, which is the username .replace(Regex("<img=\\d+>"), "") // Remove image tags
InsertMiniMenuEntry("Lookup", entry.subject, searchHiscore(username.replace(" ","_"))) .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
InsertMiniMenuEntry("Lookup", entry.subject, searchHiscore(cleanedInput))
} }
} }
} }
} }
private fun searchHiscore(username: String): Runnable {
return Runnable {
cardLayout.show(mainContentPanel, "HISCORE_SEARCH_VIEW")
StateManager.focusedView = "HISCORE_SEARCH_VIEW"
val customSearchField = hiScoreView?.let { HiscoresView.CustomSearchField(it) }
customSearchField?.searchPlayer(username) ?: run {
println("searchView is null or CustomSearchField creation failed.")
}
hiScoreView?.repaint()
}
}
override fun OnPluginsReloaded(): Boolean { override fun OnPluginsReloaded(): Boolean {
if (!initialized) return true if (!initialized) return true
updateDisplaySettings()
UpdateDisplaySettings() frame.remove(rightPanelWrapper)
frame.remove(scrollPane)
frame.layout = BorderLayout() frame.layout = BorderLayout()
frame.add(scrollPane, BorderLayout.EAST) rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
// Clear or regenerate the reflectiveEditorView
reflectiveEditorView?.removeAll()
reflectiveEditorView?.revalidate()
if(StateManager.focusedView == "REFLECTIVE_EDITOR_VIEW")
reflectiveEditorView?.repaint()
frame.revalidate() frame.revalidate()
frame.repaint()
pluginsReloaded = true pluginsReloaded = true
reloadInterfaces = true
return true return true
} }
override fun OnXPUpdate(skillId: Int, xp: Int) { override fun OnXPUpdate(skillId: Int, xp: Int) {
if (!initialXP.containsKey(skillId)) { if (!initialXP.containsKey(skillId)) {
initialXP[skillId] = xp initialXP[skillId] = xp
return return
} }
var xpWidget = xpWidgets[skillId]
if (xpWidget != null) {
updateWidget(xpWidget, xp)
} else {
val previousXp = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
var xpWidget = xpWidgets[skillId] xpWidget = createXPWidget(skillId, previousXp)
xpWidgets[skillId] = xpWidget
if (xpWidget != null) { xpTrackerView?.add(wrappedWidget(xpWidget.container))
updateWidget(xpWidget, xp) xpTrackerView?.add(Box.createVerticalStrut(5))
} else {
val previousXp = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
xpWidget = createXPWidget(skillId, previousXp) if(focusedView == XPTrackerView.VIEW_NAME) {
xpWidgets[skillId] = xpWidget xpTrackerView?.revalidate()
xpTrackerView?.repaint()
}
xpTrackerView?.add(wrappedWidget(xpWidget.panel)) updateWidget(xpWidget, xp)
xpTrackerView?.add(Box.createVerticalStrut(5)) }
xpTrackerView?.revalidate()
if(StateManager.focusedView == "XP_TRACKER_VIEW")
xpTrackerView?.repaint()
updateWidget(xpWidget, xp)
}
} }
override fun Draw(timeDelta: Long) { override fun Draw(timeDelta: Long) {
@ -221,155 +208,500 @@ class plugin : Plugin() {
} }
if (pluginsReloaded) { if (pluginsReloaded) {
InterfaceList.method3712(true) // Gets the resize working correctly
reflectiveEditorView?.let { addPlugins(it) } reflectiveEditorView?.let { addPlugins(it) }
pluginsReloaded = false pluginsReloaded = false
} }
if (reloadInterfaces){
InterfaceList.method3712(true) // Gets the resize working correctly
reloadInterfaces = false
}
accumulatedTime += timeDelta accumulatedTime += timeDelta
if (accumulatedTime >= tickInterval) { if (accumulatedTime >= TICK_INTERVAL) {
lootTrackerView?.let { onPostClientTick(it) } lootTrackerView?.let { onPostClientTick(it) }
accumulatedTime = 0L accumulatedTime = 0L
} }
// Draw synced actions (that require to be done between glBegin and glEnd)
if (drawActions.isNotEmpty()) {
synchronized(drawActions) {
val actionsCopy = drawActions.toList()
drawActions.clear()
for (action in actionsCopy) {
action()
}
}
}
// Init in the draw call so we know we are between glBegin and glEnd for HD // Init in the draw call so we know we are between glBegin and glEnd for HD
if(!initialized && mainLoadState >= loginScreen) { if(!initialized && mainLoadState >= loginScreen) {
initKondoUI() initKondoUI()
} }
} }
private fun initKondoUI(){ override fun LateDraw(timeDelta: Long) {
DrawText(FontType.LARGE, fromColor(Color(16777215)), TextModifier.CENTER, "KondoKit Loading Sprites...", GameShell.canvasWidth/2, GameShell.canvasHeight/2) if (!initialized) return
if(!allSpritesLoaded()) return; if(GameShell.fullScreenFrame != null) {
val frame: Frame? = GameShell.frame DisplayMode.setWindowMode(true, 0, FIXED_WIDTH, FIXED_HEIGHT)
if (frame != null) { showAlert("Fullscreen is not supported by KondoKit. Disable the plugin first.",
kondoExposed_useLiveGEPrices = (GetData("kondoUseRemoteGE") as? Boolean) ?: true "Error",
kondoExposed_playerXPMultiplier = (GetData("kondoPlayerXPMultiplier") as? Int) ?: 5 JOptionPane.INFORMATION_MESSAGE
cardLayout = CardLayout() )
mainContentPanel = Panel(cardLayout) return
mainContentPanel.background = VIEW_BACKGROUND_COLOR
xpTrackerView = createXPTrackerView()
hiScoreView = createHiscoreSearchView()
lootTrackerView = createLootTrackerView()
reflectiveEditorView = createReflectiveEditorView()
mainContentPanel.add(xpTrackerView, "XP_TRACKER_VIEW")
mainContentPanel.add(hiScoreView, "HISCORE_SEARCH_VIEW")
mainContentPanel.add(lootTrackerView, "LOOT_TRACKER_VIEW")
mainContentPanel.add(reflectiveEditorView, "REFLECTIVE_EDITOR_VIEW")
val navPanel = Panel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR
preferredSize = Dimension(42, frame.height)
}
navPanel.add(createNavButton(LVL_ICON, "XP_TRACKER_VIEW"))
navPanel.add(createNavButton(MAG_SPRITE, "HISCORE_SEARCH_VIEW"))
navPanel.add(createNavButton(LOOT_ICON, "LOOT_TRACKER_VIEW"))
navPanel.add(createNavButton(WRENCH_ICON, "REFLECTIVE_EDITOR_VIEW"))
val rightPanel = Panel(BorderLayout()).apply {
add(mainContentPanel, BorderLayout.CENTER)
add(navPanel, BorderLayout.EAST)
}
scrollPane = JScrollPane(rightPanel).apply {
preferredSize = Dimension(SCROLLPANE_WIDTH, frame.height)
background = VIEW_BACKGROUND_COLOR
border = BorderFactory.createEmptyBorder() // Removes the border completely
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER
}
frame.layout = BorderLayout()
scrollPane?.let { frame.add(it, BorderLayout.EAST) }
frame.revalidate()
frame.repaint()
StateManager.focusedView = "XP_TRACKER_VIEW"
initialized = true
pluginsReloaded = true
UpdateDisplaySettings()
} }
if(!useScaledFixed) return
if(GetWindowMode() == WindowMode.FIXED){
moveAltCanvasToFront()
} else {
moveCanvasToFront()
}
altCanvas?.updateGameImage() // Update the game image as needed
} }
override fun Update() { override fun Update() {
xpWidgets.values.forEach { xpWidget ->
val widgets = xpWidgets.values
val totalXP = totalXPWidget
widgets.forEach { xpWidget ->
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0 val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0 val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedXpPerHour = formatNumber(xpPerHour) val formattedXpPerHour = formatNumber(xpPerHour)
xpWidget.xpPerHourLabel.text = xpWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor) formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
xpWidget.panel.repaint() xpWidget.container.repaint()
} }
totalXPWidget?.let { totalXPWidget -> totalXP?.let { totalXPWidget ->
val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0 val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0
val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0 val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedTotalXpPerHour = formatNumber(totalXPPerHour) val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
totalXPWidget.xpPerHourLabel.text = totalXPWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor) formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
totalXPWidget.panel.repaint() totalXPWidget.container.repaint()
} }
} }
override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) { override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) {
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z)) val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
npcDeathSnapshots[npcID] = LootTrackerView.GroundSnapshot(preDeathSnapshot, Pair(x, z), 0) npcDeathSnapshots[npcID] = LootTrackerView.GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
} }
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)
for (icon in otherIcons) {
if(!js5Archive8.isFileReady(icon)){
return false
}
}
} catch (e : Exception){
return false
}
return true
}
private fun createNavButton(spriteId: Int, viewName: String): JButton { private fun initAltCanvas(){
val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId)) if (frame != null) {
val buttonSize = Dimension(42, 42) altCanvas = AltCanvas().apply {
val imageSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height) preferredSize = Dimension(FIXED_WIDTH, FIXED_HEIGHT)
}
altCanvas?.let { frame.add(it) }
moveAltCanvasToFront()
frame.setComponentZOrder(rightPanelWrapper, 2)
}
updateDisplaySettings()
}
val actionListener = ActionListener { private fun updateDisplaySettings() {
cardLayout.show(mainContentPanel, viewName) val mode = GetWindowMode()
StateManager.focusedView = viewName val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH
lastUIOffset = uiOffset
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()
}
fun OnKondoValueUpdated(){
StoreData("kondoUseRemoteGE", useLiveGEPrices)
StoreData("kondoTheme", theme.toString())
if(appliedTheme != theme) {
showAlert(
"KondoKit Theme changes require a relaunch.",
"KondoKit",
JOptionPane.INFORMATION_MESSAGE
)
}
StoreData("kondoPlayerXPMultiplier", playerXPMultiplier)
LootTrackerView.gePriceMap = LootTrackerView.loadGEPrices()
StoreData("kondoLaunchMinimized", launchMinimized)
StoreData("kondoUIOffset", uiOffset)
StoreData("kondoScaledFixed", useScaledFixed)
if(altCanvas == null && useScaledFixed){
initAltCanvas()
} else if(altCanvas != null && !useScaledFixed){
destroyAltCanvas()
}
if(lastUIOffset != uiOffset){
updateDisplaySettings()
reloadInterfaces = true
}
}
private fun destroyAltCanvas(){
moveCanvasToFront()
frame.remove(altCanvas)
altCanvas = null
updateDisplaySettings()
}
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.")
}
}
}
private fun moveAltCanvasToFront(){
if(altCanvas == null) return
frame.setComponentZOrder(canvas, 2)
frame.setComponentZOrder(altCanvas, 1)
frame.setComponentZOrder(rightPanelWrapper, 0)
}
private fun moveCanvasToFront(){
if(altCanvas == null) return
frame.setComponentZOrder(altCanvas, 2)
frame.setComponentZOrder(canvas, 1)
frame.setComponentZOrder(rightPanelWrapper, 0)
}
private fun restoreSettings(){
themeName = (GetData("kondoTheme") as? String) ?: "RUNELITE"
useLiveGEPrices = (GetData("kondoUseRemoteGE") as? Boolean) ?: true
playerXPMultiplier = (GetData("kondoPlayerXPMultiplier") as? Int) ?: 5
val osName = System.getProperty("os.name").toLowerCase()
uiOffset = (GetData("kondoUIOffset") as? Int) ?: if (osName.contains("win")) 16 else 0
launchMinimized = (GetData("kondoLaunchMinimized") as? Boolean) ?: false
useScaledFixed = (GetData("kondoScaledFixed") as? Boolean) ?: false
}
private fun initKondoUI(){
DrawText(FontType.LARGE, fromColor(Color(16777215)), TextModifier.CENTER, "KondoKit Loading Sprites...", GameShell.canvasWidth/2, GameShell.canvasHeight/2)
if(!allSpritesLoaded()) return
val frame: Frame? = GameShell.frame
if (frame != null) {
restoreSettings()
theme = ThemeType.valueOf(themeName)
applyTheme(getTheme(theme))
appliedTheme = theme
configureLookAndFeel()
cardLayout = CardLayout()
mainContentPanel = JPanel(cardLayout).apply {
border = BorderFactory.createEmptyBorder(0, 0, 0, 0) // Removes any default border or padding
background = VIEW_BACKGROUND_COLOR
preferredSize = Dimension(MAIN_CONTENT_WIDTH, frame.height)
isOpaque = true
}
// Register Views
createXPTrackerView()
createHiscoreSearchView()
createLootTrackerView()
createReflectiveEditorView()
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)
val navPanel = Panel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR
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))
val rightPanel = Panel(BorderLayout()).apply {
add(mainContentPanel, BorderLayout.CENTER)
add(navPanel, BorderLayout.EAST)
}
rightPanelWrapper = JScrollPane(rightPanel).apply {
preferredSize = Dimension(NAVBAR_WIDTH + MAIN_CONTENT_WIDTH, frame.height)
background = VIEW_BACKGROUND_COLOR
border = BorderFactory.createEmptyBorder()
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER
}
frame.layout = BorderLayout()
rightPanelWrapper?.let {
frame.add(it, BorderLayout.EAST)
}
if(launchMinimized){
setActiveView(HIDDEN_VIEW)
} else {
setActiveView(XPTrackerView.VIEW_NAME)
}
if(useScaledFixed) {
initAltCanvas()
}
initialized = true
pluginsReloaded = true
}
}
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
}
cardLayout.show(mainContentPanel, viewName)
}
reloadInterfaces = true
updateDisplaySettings()
// Revalidate and repaint necessary panels
mainContentPanel.revalidate()
rightPanelWrapper?.revalidate()
frame?.revalidate()
mainContentPanel.repaint()
rightPanelWrapper?.repaint()
frame?.repaint()
focusedView = viewName
}
private fun createNavButton(spriteId: Int, viewName: String): JPanel {
val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId), NAV_TINT, NAV_GREYSCALE, BOOST)
val buttonSize = Dimension(NAVBAR_WIDTH, 32)
val imageSize = Dimension((bufferedImageSprite.width / 1.2f).toInt(), (bufferedImageSprite.height / 1.2f).toInt())
val cooldownDuration = 100L
val actionListener = ActionListener {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < cooldownDuration) {
return@ActionListener
}
lastClickTime = currentTime
if (focusedView == viewName) {
setActiveView("HIDDEN")
} else {
setActiveView(viewName)
}
}
// ImageCanvas with forced size
val imageCanvas = ImageCanvas(bufferedImageSprite).apply { val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
background = WIDGET_COLOR background = WIDGET_COLOR
preferredSize = imageSize preferredSize = imageSize
maximumSize = imageSize maximumSize = imageSize
minimumSize = imageSize minimumSize = imageSize
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
actionListener.actionPerformed(null)
}
})
} }
val button = JButton().apply { // 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
isOpaque = false // No background for the wrapper
add(imageCanvas) // Adding ImageCanvas directly, layout won't stretch it
}
val panelButton = JPanel().apply {
layout = GridBagLayout() layout = GridBagLayout()
preferredSize = buttonSize preferredSize = buttonSize
maximumSize = buttonSize maximumSize = buttonSize
minimumSize = buttonSize minimumSize = buttonSize
background = WIDGET_COLOR background = WIDGET_COLOR
isFocusPainted = false isOpaque = true
isBorderPainted = false
val gbc = GridBagConstraints().apply { val gbc = GridBagConstraints().apply {
anchor = GridBagConstraints.CENTER anchor = GridBagConstraints.CENTER
fill = GridBagConstraints.NONE // Prevents stretching
} }
add(imageCanvas, gbc) add(imageCanvasWrapper, gbc)
addActionListener(actionListener)
// Hover and click behavior
val hoverListener = object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent?) {
background = WIDGET_COLOR.darker()
imageCanvas.fillColor = WIDGET_COLOR.darker()
imageCanvas.repaint()
repaint()
}
override fun mouseExited(e: MouseEvent?) {
background = WIDGET_COLOR
imageCanvas.fillColor = WIDGET_COLOR
imageCanvas.repaint()
repaint()
}
override fun mouseClicked(e: MouseEvent?) {
actionListener.actionPerformed(null)
}
}
addMouseListener(hoverListener)
imageCanvas.addMouseListener(hoverListener)
} }
return button return panelButton
}
private fun configureLookAndFeel(){
loadFont()
try {
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel")
// Modify the UI properties to match theme
UIManager.put("control", VIEW_BACKGROUND_COLOR)
UIManager.put("info", VIEW_BACKGROUND_COLOR)
UIManager.put("nimbusBase", WIDGET_COLOR)
UIManager.put("nimbusBlueGrey", TITLE_BAR_COLOR)
UIManager.put("nimbusDisabledText", primaryColor)
UIManager.put("nimbusSelectedText", secondaryColor)
UIManager.put("text", secondaryColor)
UIManager.put("nimbusFocus", TITLE_BAR_COLOR)
UIManager.put("nimbusInfoBlue", POPUP_BACKGROUND)
UIManager.put("nimbusLightBackground", WIDGET_COLOR)
UIManager.put("nimbusSelectionBackground", PROGRESS_BAR_FILL)
UIManager.put("Button.background", WIDGET_COLOR)
UIManager.put("Button.foreground", secondaryColor)
UIManager.put("CheckBox.background", VIEW_BACKGROUND_COLOR)
UIManager.put("CheckBox.foreground", secondaryColor)
UIManager.put("CheckBox.icon", UIManager.getIcon("CheckBox.icon"))
UIManager.put("ComboBox.background", WIDGET_COLOR)
UIManager.put("ComboBox.foreground", secondaryColor)
UIManager.put("ComboBox.selectionBackground", PROGRESS_BAR_FILL)
UIManager.put("ComboBox.selectionForeground", primaryColor)
UIManager.put("ComboBox.buttonBackground", WIDGET_COLOR)
UIManager.put("Spinner.background", WIDGET_COLOR)
UIManager.put("Spinner.foreground", secondaryColor)
UIManager.put("Spinner.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR))
UIManager.put("TextField.background", WIDGET_COLOR)
UIManager.put("TextField.foreground", secondaryColor)
UIManager.put("TextField.caretForeground", secondaryColor)
UIManager.put("TextField.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR))
UIManager.put("ScrollBar.thumb", WIDGET_COLOR)
UIManager.put("ScrollBar.track", VIEW_BACKGROUND_COLOR)
UIManager.put("ScrollBar.thumbHighlight", TITLE_BAR_COLOR)
UIManager.put("ProgressBar.foreground", PROGRESS_BAR_FILL)
UIManager.put("ProgressBar.background", WIDGET_COLOR)
UIManager.put("ProgressBar.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR))
UIManager.put("ToolTip.background", VIEW_BACKGROUND_COLOR)
UIManager.put("ToolTip.foreground", secondaryColor)
UIManager.put("ToolTip.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR))
// Update component tree UI to apply the new theme
SwingUtilities.updateComponentTreeUI(frame)
frame.background = Color.BLACK
} catch (e : Exception) {
e.printStackTrace()
}
}
private fun loadFont(): Font? {
val fontStream = plugin::class.java.getResourceAsStream("res/runescape_small.ttf")
return if (fontStream != null) {
try {
val font = Font.createFont(Font.TRUETYPE_FONT, fontStream)
val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
ge.registerFont(font) // Register the font in the graphics environment
font
} catch (e: Exception) {
e.printStackTrace()
null
}
} else {
println("Font not found!")
null
}
} }
object StateManager { object StateManager {
val initialXP: MutableMap<Int, Int> = HashMap()
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
var totalXPWidget: XPWidget? = null
var focusedView: String = "" var focusedView: String = ""
} }
private fun applyTheme(theme: Theme) {
WIDGET_COLOR = theme.widgetColor
TITLE_BAR_COLOR = theme.titleBarColor
VIEW_BACKGROUND_COLOR = theme.viewBackgroundColor
primaryColor = theme.primaryColor
secondaryColor = theme.secondaryColor
POPUP_BACKGROUND = theme.popupBackground
POPUP_FOREGROUND = theme.popupForeground
TOOLTIP_BACKGROUND = theme.tooltipBackground
SCROLL_BAR_COLOR = theme.scrollBarColor
PROGRESS_BAR_FILL = theme.progressBarFill
NAV_TINT = theme.navTint
NAV_GREYSCALE = theme.navGreyScale
BOOST = theme.boost
}
} }

View file

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

View file

@ -25,6 +25,7 @@ class plugin : Plugin() {
private var timeMode = TIME_MODE_INITIALIZATION private var timeMode = TIME_MODE_INITIALIZATION
private var initTime: Long = 0 private var initTime: Long = 0
private var logoutFlag = true
private var displayMessageCounter = 0 private var displayMessageCounter = 0
private var component: Component? = null private var component: Component? = null
@ -35,6 +36,20 @@ class plugin : Plugin() {
displayMessageCounter = 0 displayMessageCounter = 0
} }
override fun OnPluginsReloaded(): Boolean {
return true
}
override fun OnLogin() {
if(logoutFlag)
initTime = System.currentTimeMillis()
logoutFlag = false
}
override fun OnLogout() {
logoutFlag = true
}
override fun Draw(timeDelta: Long) { override fun Draw(timeDelta: Long) {
if (component == null) if (component == null)
return return
@ -134,6 +149,9 @@ class plugin : Plugin() {
API.InsertMiniMenuEntry("Disable Timer", "") { API.InsertMiniMenuEntry("Disable Timer", "") {
timeMode = DEFAULT_TIME_MODE timeMode = DEFAULT_TIME_MODE
} }
API.InsertMiniMenuEntry("Reset Play Time", "") {
initTime = System.currentTimeMillis()
}
} }
} }
} }

View file

@ -1,3 +1,3 @@
AUTHOR='Woahscam, Ceikry' AUTHOR='Woahscam, Ceikry'
DESCRIPTION='Displays the session time played, system time, or no time over the "Report Abuse" button.' DESCRIPTION='Displays the session time played, system time, or no time over the "Report Abuse" button.'
VERSION=1.2 VERSION=1.1

View file

@ -1,52 +1,56 @@
package ToggleResizableSD package ToggleResizableSD
import KondoKit.Exposed
import plugin.Plugin import plugin.Plugin
import plugin.annotations.PluginMeta
import plugin.api.API import plugin.api.API
import rt4.* import plugin.api.API.StoreData
import rt4.DisplayMode
import rt4.GameShell
import rt4.InterfaceList
import rt4.client
import java.awt.event.KeyAdapter import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
class plugin : Plugin() { class plugin : Plugin() {
var toggleResize = false
var wantHd = false //Setting wantHd to true hides the black screen on logout (when resize SD is enabled), by enabling HD on logout @Exposed("Use Resizable SD")
var useResizable = false
@Exposed("Setting wantHd to true hides the black screen on logout (when resize SD is enabled), by enabling HD on logout ")
var wantHd = false
override fun Init() { override fun Init() {
API.AddKeyboardListener(object : KeyAdapter() { API.AddKeyboardListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) { override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_F12) { if (e.keyCode == KeyEvent.VK_F12) {
toggleResize = true toggleResizableSd()
} }
} }
}) })
if (!DisplayMode.resizableSD && API.GetData("use-resizable-sd") == true) {
toggleResize = true
}
var osNameLowerCase: String = ""
var osName: String
try { useResizable = DisplayMode.resizableSD
osName = System.getProperty("os.name") if (API.GetData("use-resizable-sd") == true) {
} catch (e: Exception) { useResizable = true
osName = "Unknown"
} }
osNameLowerCase = osName.toLowerCase() var osNameLowerCase: String = System.getProperty("os.name").toLowerCase()
if (!osNameLowerCase.startsWith("mac")) { if (!osNameLowerCase.startsWith("mac")) {
wantHd = true wantHd = true
} }
if (API.GetData("want-hd") == false) { if (API.GetData("want-hd") == false) {
wantHd = false wantHd = false
} }
} }
override fun ProcessCommand(commandStr: String, args: Array<out String>?) { override fun ProcessCommand(commandStr: String, args: Array<out String>?) {
when(commandStr.toLowerCase()) { when (commandStr.toLowerCase()) {
"::toggleresizablesd", "::resizablesd", "::togglersd", "::rsd" -> { "::toggleresizablesd", "::resizablesd", "::togglersd", "::rsd" -> {
toggleResize = true //We could call toggleResizableSd() directly here, but it's not necessary. toggleResizableSd()
} }
"::toggleresizablesdhd", "::resizablesdhd", "::togglersdhd", "::rsdhd", -> { "::toggleresizablesdhd", "::resizablesdhd", "::togglersdhd", "::rsdhd" -> {
wantHd = !wantHd wantHd = !wantHd
API.StoreData("want-hd", wantHd) StoreData("want-hd", wantHd)
API.SendMessage("You have turned login screen HD " + (if (wantHd) "on" else "off")) API.SendMessage("You have turned login screen HD " + (if (wantHd) "on" else "off"))
} }
} }
@ -57,25 +61,29 @@ class plugin : Plugin() {
if (InterfaceList.aClass13_26 == null || client.gameState != 30) { if (InterfaceList.aClass13_26 == null || client.gameState != 30) {
return return
} }
toggleResize = false
DisplayMode.resizableSD = !DisplayMode.resizableSD; DisplayMode.resizableSD = !DisplayMode.resizableSD
if(!DisplayMode.resizableSD){ useResizable = DisplayMode.resizableSD
//Revert to fixed StoreData("use-resizable-sd", useResizable)
API.StoreData("use-resizable-sd", false) //Note: It is important to call StoreData before setWindowMode because setWindowMode causes all plugins to reload.
if (!DisplayMode.resizableSD) {
DisplayMode.setWindowMode(true, 0, -1, -1) DisplayMode.setWindowMode(true, 0, -1, -1)
} else { } else {
//Use resizable
API.StoreData("use-resizable-sd", true) //Note: It is important to call StoreData before setWindowMode because setWindowMode causes all plugins to reload.
DisplayMode.setWindowMode(true, 0, GameShell.frameWidth, GameShell.frameHeight) DisplayMode.setWindowMode(true, 0, GameShell.frameWidth, GameShell.frameHeight)
} }
} }
override fun Draw(timeDelta: Long) { override fun Draw(timeDelta: Long) {
if (toggleResize) { if (useResizable != DisplayMode.resizableSD) {
toggleResizableSd() toggleResizableSd()
} }
} }
fun OnKondoValueUpdated() {
StoreData("want-hd", wantHd)
StoreData("use-resizable-sd", useResizable)
}
override fun OnLogout() { override fun OnLogout() {
if (DisplayMode.resizableSD && wantHd) { if (DisplayMode.resizableSD && wantHd) {
//Because resizable SD always uses the "HD" size canvas/window mode (check the in-game Graphics Options with resizeable SD enabled if you don't believe me!), useHD becomes true when logging out, so logging out with resizeSD enabled means "HD" will always be enabled on the login screen after logging out, so we might as well fix the HD flyover by setting resizableSD to false first, and then calling setWindowMode to replace the canvas and set newMode to 2. //Because resizable SD always uses the "HD" size canvas/window mode (check the in-game Graphics Options with resizeable SD enabled if you don't believe me!), useHD becomes true when logging out, so logging out with resizeSD enabled means "HD" will always be enabled on the login screen after logging out, so we might as well fix the HD flyover by setting resizableSD to false first, and then calling setWindowMode to replace the canvas and set newMode to 2.

View file

@ -1,3 +1,3 @@
AUTHOR='ipkpjersi' AUTHOR='ipkpjersi'
DESCRIPTION='Allows you to use F12 to toggle resizable SD.' DESCRIPTION='Allows you to use F12 to toggle resizable SD.'
VERSION=1.0 VERSION=1.1

View file

@ -1,12 +1,34 @@
package XPDropPlugin; package XPDropPlugin
import KondoKit.Exposed
import plugin.Plugin import plugin.Plugin
import plugin.annotations.PluginMeta import plugin.api.API
import plugin.api.* import plugin.api.API.*
import plugin.api.FontColor.fromColor
import plugin.api.FontType
import plugin.api.TextModifier
import plugin.api.WindowMode
import rt4.Sprite
import rt4.client
import java.awt.Color import java.awt.Color
import java.awt.image.BufferedImage
import java.io.InputStream
import javax.imageio.ImageIO
import kotlin.math.ceil import kotlin.math.ceil
class plugin : Plugin() { class plugin : Plugin() {
enum class Theme {
DEFAULT, RUNELITE
}
@Exposed
private var theme = Theme.DEFAULT
@Exposed
private var alwaysShow = false
private val displayTimeout = 10000L // 10 seconds private val displayTimeout = 10000L // 10 seconds
private val drawStart = 175 private val drawStart = 175
private val drawPadding = 25 private val drawPadding = 25
@ -15,41 +37,50 @@ class plugin : Plugin() {
private var totalXp = 0 private var totalXp = 0
private val activeGains = ArrayList<XPGain>() private val activeGains = ArrayList<XPGain>()
private var lastGain = 0L private var lastGain = 0L
private val IN_GAME = 30
private val spriteCache = HashMap<Int, Sprite?>()
override fun Init() {
val themeIndex = (GetData("xp-drop-theme") as? String) ?: "DEFAULT"
theme = Theme.valueOf(themeIndex)
alwaysShow = (GetData("xp-drop-alwaysShow") as? Boolean) ?: false
}
override fun Draw(deltaTime: Long) { override fun Draw(deltaTime: Long) {
if (System.currentTimeMillis() - lastGain >= displayTimeout && activeGains.isEmpty()) if (shouldSkipDrawing()) return
return
drawTotalXPBox() drawTotalXPBox()
val removeList = ArrayList<XPGain>() val removeList = ArrayList<XPGain>()
var posX = API.GetWindowDimensions().width / 2
if (API.GetWindowMode() == WindowMode.FIXED) val movementSpeedFactor = deltaTime / 16.666 // 60 FPS
posX += 60
for(gain in activeGains) { for (gain in activeGains) {
gain.currentPos -= ceil(deltaTime / 20.0).toInt() gain.currentPos -= ceil(movementSpeedFactor).toInt() // Adjust movement based on deltaTime
if (gain.currentPos <= drawClear) { if (gain.currentPos <= drawClear) {
removeList.add(gain) removeList.add(gain)
totalXp += gain.xp totalXp += gain.xp
} else if (gain.currentPos <= drawStart){ } else if (gain.currentPos <= drawStart) {
val sprite = XPSprites.getSpriteForSkill(skillId = gain.skill) drawXPDrops(gain)
sprite?.render(posX - 25, gain.currentPos - 20)
API.DrawText(
FontType.SMALL,
FontColor.fromColor(Color.WHITE),
TextModifier.LEFT,
addCommas(gain.xp.toString()),
posX,
gain.currentPos
)
} }
} }
activeGains.removeAll(removeList.toSet()) activeGains.removeAll(removeList.toSet())
} }
private fun shouldSkipDrawing(): Boolean {
return client.gameState < IN_GAME || (!alwaysShow && isDisplayTimeoutExpired() && activeGains.isEmpty())
}
fun OnKondoValueUpdated() {
StoreData("xp-drop-theme",theme.toString())
StoreData("xp-drop-alwaysShow",alwaysShow)
}
private fun isDisplayTimeoutExpired(): Boolean {
return System.currentTimeMillis() - lastGain >= displayTimeout
}
override fun OnXPUpdate(skill: Int, xp: Int) { override fun OnXPUpdate(skill: Int, xp: Int) {
if (xp == lastXp[skill]) return if (xp == lastXp[skill]) return
@ -81,6 +112,54 @@ class plugin : Plugin() {
} }
private fun drawTotalXPBox() { private fun drawTotalXPBox() {
when (theme) {
Theme.DEFAULT -> drawDefaultXPBox()
Theme.RUNELITE -> drawRuneliteXPBox()
}
}
private fun drawXPDrops(gain : XPGain) {
when (theme) {
Theme.DEFAULT -> drawDefaultXPDrop(gain)
Theme.RUNELITE -> drawRuneliteXPDrops(gain)
}
}
private fun drawDefaultXPDrop(gain: XPGain) {
var posX = API.GetWindowDimensions().width / 2
if (API.GetWindowMode() == WindowMode.FIXED)
posX += 60
val sprite = spriteCache.getOrPut(gain.skill) { XPSprites.getSpriteForSkill(skillId = gain.skill) }
sprite?.render(posX - 25, gain.currentPos - 20)
DrawText(
FontType.SMALL,
fromColor(Color.WHITE),
TextModifier.LEFT,
addCommas(gain.xp.toString()),
posX,
gain.currentPos
)
}
private fun drawRuneliteXPDrops(gain: XPGain) {
val w = API.GetWindowDimensions().width
val offset = if(API.GetWindowMode() == WindowMode.FIXED) 251 else 225
val extra = 2;
val posX = w - (offset + extra)
val str = addCommas(gain.xp.toString())
val fontCharWidth = 4
val displace = str.length*fontCharWidth + 30
// should be scaled https://github.com/runelite/runelite/blob/0500906f8de9cd20875c168a7a59e5e066ed5058/runelite-client/src/main/java/net/runelite/client/game/SkillIconManager.java#L50
// but for now this is good enough
val sprite = spriteCache.getOrPut(gain.skill) { XPSprites.getSpriteForSkill(skillId = gain.skill) }
sprite?.render(posX - displace, gain.currentPos - 20)
drawTextWithDropShadow(posX, gain.currentPos, Color.WHITE, addCommas(gain.xp.toString()))
}
private fun drawDefaultXPBox() {
var posX = API.GetWindowDimensions().width / 2 var posX = API.GetWindowDimensions().width / 2
val posY = API.GetWindowDimensions().height / 4 val posY = API.GetWindowDimensions().height / 4
@ -89,13 +168,13 @@ class plugin : Plugin() {
API.ClipRect(0, 0, posX * 2, posY * 4) API.ClipRect(0, 0, posX * 2, posY * 4)
val horizontal = API.GetSprite(822) val horizontal = spriteCache.getOrPut(822) { API.GetSprite(822) }
val horizontalTop = API.GetSprite(820) val horizontalTop = spriteCache.getOrPut(820) { API.GetSprite(820) }
val tlCorner = API.GetSprite(824) val tlCorner = spriteCache.getOrPut(824) { API.GetSprite(824) }
val blCorner = API.GetSprite(826) val blCorner = spriteCache.getOrPut(826) { API.GetSprite(826) }
val trCorner = API.GetSprite(825) val trCorner = spriteCache.getOrPut(825) { API.GetSprite(825) }
val brCorner = API.GetSprite(827) val brCorner = spriteCache.getOrPut(827) { API.GetSprite(827) }
val bg = API.GetSprite(657) val bg = spriteCache.getOrPut(657) { API.GetSprite(657) }
bg?.render(posX - 77, 10) bg?.render(posX - 77, 10)
API.FillRect(posX - 75, 5, 140, 30, 0, 64) API.FillRect(posX - 75, 5, 140, 30, 0, 64)
@ -112,26 +191,58 @@ class plugin : Plugin() {
horizontalTop?.render(posX + 9, -8) horizontalTop?.render(posX + 9, -8)
horizontal?.render(posX + 9, 22) horizontal?.render(posX + 9, 22)
API.DrawText( DrawText(
FontType.SMALL, FontType.SMALL,
FontColor.fromColor(Color.WHITE), fromColor(Color.WHITE),
TextModifier.LEFT, TextModifier.LEFT,
"Total Xp: ${addCommas(totalXp.toString())}", "Total Xp: ${addCommas(totalXp.toString())}",
posX - 65, posX - 65,
28 28
) )
} }
private fun drawRuneliteXPBox() {
val boxHeight = 29
val boxWidth = 119
val posX = API.GetWindowDimensions().width
val innerBorderColor = Color(90, 82, 69).rgb
val outerBorderColor = Color(56,48,35).rgb
val offset = if(API.GetWindowMode() == WindowMode.FIXED) 251 else 225
val boxStart = posX - (offset + boxWidth)
val yOffset = if(API.GetWindowMode() == WindowMode.FIXED) 4 else 0
val lvlIcon = 898;
val sprite = spriteCache.getOrPut(lvlIcon){
val imageStream: InputStream = plugin::class.java.getResourceAsStream("res/rl-lvls.png")
imageStream.use { imageStream ->
val image: BufferedImage = ImageIO.read(imageStream)
API.GetSpriteFromPNG(image)
}
}
// Draw a simple rectangle instead of the default box design
API.FillRect(boxStart, yOffset, boxWidth, boxHeight, innerBorderColor, 150)
drawTextWithDropShadow(boxStart+boxWidth-4, 18+yOffset, Color.WHITE, addCommas(totalXp.toString()))
// Inner Border
API.DrawRect(boxStart+1, 1+yOffset, boxWidth-2, boxHeight-2, innerBorderColor)
// redraw around the border
API.DrawRect(boxStart, yOffset, boxWidth, boxHeight, outerBorderColor)
sprite?.render(boxStart + 3, 3+yOffset)
}
data class XPGain(val skill: Int, val xp: Int, var currentPos: Int) data class XPGain(val skill: Int, val xp: Int, var currentPos: Int)
fun addCommas(num: String): String{ fun addCommas(num: String): String {
var newString = "" var newString = ""
if(num.length > 9){ if (num.length > 9) {
return "Lots!" return "Lots!"
} }
var counter = 1 var counter = 1
num.reversed().forEach { num.reversed().forEach {
if(counter % 3 == 0 && counter != num.length){ if (counter % 3 == 0 && counter != num.length) {
newString += "$it," newString += "$it,"
} else { } else {
newString += it newString += it
@ -140,4 +251,10 @@ class plugin : Plugin() {
} }
return newString.reversed() return newString.reversed()
} }
private fun drawTextWithDropShadow(x: Int, y: Int, color: Color, text: String, mod : TextModifier = TextModifier.RIGHT) {
DrawText(FontType.SMALL, fromColor(Color(0)), mod, text, x + 1, y + 1)
DrawText(FontType.SMALL, fromColor(color), mod, text, x, y)
}
} }

View file

@ -1,3 +1,3 @@
AUTHOR='Ceikry' AUTHOR='Ceikry'
DESCRIPTION='Draws nice and clean experience drops onto the screen.' DESCRIPTION='Draws nice and clean experience drops onto the screen.'
VERSION=1.2 VERSION=1.3

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B