Merge branch 'KondoKit-2.0' into 'master'

KondoKit 2.0

See merge request 2009scape/rt4-client!28
This commit is contained in:
downthecrop 2024-11-03 18:39:03 +00:00
commit dbde09f319
36 changed files with 2491 additions and 869 deletions

View file

@ -13,6 +13,7 @@ import rt4.Tile;
*/
public abstract class Plugin {
long timeOfLastDraw;
long timeOfLastLateDraw;
void _init() {
Init();
@ -24,6 +25,12 @@ public abstract class Plugin {
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.
* 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) {}
/**
* 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
*/

View file

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

View file

@ -160,6 +160,11 @@ public class PluginRepository {
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) {
loadedPlugins.values().forEach((plugin) -> plugin.NPCOverheadDraw(npc, screenX, screenY));
}

View file

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

View file

@ -9,6 +9,7 @@ import org.openrs2.deob.annotation.OriginalMember;
import org.openrs2.deob.annotation.Pc;
import java.awt.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.charset.StandardCharsets;
@ -24,6 +25,11 @@ public final class GlRenderer {
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")
private static float aFloat30;
@ -200,10 +206,34 @@ public final class GlRenderer {
public static void swapBuffers() {
try {
drawable.swapBuffers();
readPixels();
} 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")
public static void setFogEnabled(@OriginalArg(0) boolean enabled) {
if (enabled == fogEnabled) {

View file

@ -921,6 +921,7 @@ public final class client extends GameShell {
Preferences.safeMode = false;
Preferences.write(GameShell.signLink);
}
PluginRepository.LateDraw();
}
@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"
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
import KondoKit.Exposed
import plugin.Plugin
import plugin.annotations.PluginMeta
import plugin.api.API.*
import plugin.api.FontColor.fromColor
import plugin.api.FontType
@ -18,27 +18,26 @@ import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import kotlin.math.roundToInt
@PluginMeta(
author = "downthecrop",
description =
"""
Ground Items Overlay. Just like Runelite!
cmds ::set(low,med,high,insane,hide), ::(tag,ignore)item ID, ::(reset)groundconfig
Special thanks to Chisato for the original skeleton.
""",
version = 1.2
)
open class plugin : Plugin() {
class plugin : Plugin() {
@Exposed(description = "Default: true, Use Local JSON or the prices from the Live/Stable server API")
private var useLiveGEPrices = true
@Exposed( "Default: 5,000 (blue)")
private var lowValue = 5000
@Exposed( "Default: 20,000 (green)")
private var mediumValue = 20000
@Exposed( "Default: 50,000 (orange)")
private var highValue = 50000
@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 lateinit var kondoExposed_taggedItems: List<Int>
private lateinit var kondoExposed_ignoredItems: List<Int>
private var gePriceMap = loadGEPrices()
@ -60,23 +59,23 @@ open class plugin : Plugin() {
)
override fun Init() {
kondoExposed_lowValue = GetData("low-value") as? Int ?: 5000
kondoExposed_mediumValue = GetData("medium-value") as? Int ?: 20000
kondoExposed_highValue = GetData("high-value") as? Int ?: 50000
kondoExposed_insaneValue = GetData("insane-value") as? Int ?: 100000
kondoExposed_hideBelowValue = GetData("hide-below-value") as? Int ?: 0
kondoExposed_useLiveGEPrices = GetData("ground-item-use-remote") as? Boolean ?: true
kondoExposed_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()
if (gePriceMap.isEmpty()) SendMessage("Ground Items unable to load GE Prices, Remote: $kondoExposed_useLiveGEPrices")
lowValue = GetData("low-value") as? Int ?: 5000
mediumValue = GetData("medium-value") as? Int ?: 20000
highValue = GetData("high-value") as? Int ?: 50000
insaneValue = GetData("insane-value") as? Int ?: 100000
hideBelowValue = GetData("hide-below-value") as? Int ?: 0
useLiveGEPrices = GetData("ground-item-use-remote") as? Boolean ?: true
taggedItems = GetData("ground-item-tags")?.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: $useLiveGEPrices")
}
private fun isTagged(itemId: Int): Boolean {
return kondoExposed_taggedItems.contains(itemId)
return taggedItems.contains(itemId)
}
private fun isHidden(itemId: Int): Boolean {
return kondoExposed_ignoredItems.contains(itemId)
return ignoredItems.contains(itemId)
}
override fun Draw(timeDelta: Long) = renderGroundItemNames()
@ -141,10 +140,10 @@ open class plugin : Plugin() {
val screenY = screenPos[1]
val color = when {
isTagged(itemDef.id) -> colorMap["tagged"]
highestValue < kondoExposed_lowValue -> "#FFFFFF"
highestValue < kondoExposed_mediumValue -> colorMap["lowValue"]
highestValue < kondoExposed_highValue -> colorMap["mediumValue"]
highestValue < kondoExposed_insaneValue -> colorMap["highValue"]
highestValue < lowValue -> "#FFFFFF"
highestValue < mediumValue -> colorMap["lowValue"]
highestValue < highValue -> colorMap["mediumValue"]
highestValue < insaneValue -> colorMap["highValue"]
else -> colorMap["insaneValue"]
} ?: "#FFFFFF"
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 geValue = (gePriceMap[itemDef.id.toString()]?.toInt() ?: 0) * item.value.amount
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>?) {
@ -200,7 +199,7 @@ open class plugin : Plugin() {
private fun ignoreItem(itemId: Int): Runnable {
return Runnable {
val existingIgnores = kondoExposed_ignoredItems.toMutableList()
val existingIgnores = ignoredItems.toMutableList()
if (existingIgnores.contains(itemId)) {
existingIgnores.remove(itemId)
@ -215,7 +214,7 @@ open class plugin : Plugin() {
private fun tagItem(itemId: Int): Runnable {
return Runnable {
val existingTags = kondoExposed_taggedItems.toMutableList()
val existingTags = taggedItems.toMutableList()
if (existingTags.contains(itemId)) {
existingTags.remove(itemId)
@ -230,28 +229,28 @@ open class plugin : Plugin() {
private fun resetConfig() {
kondoExposed_lowValue = 5000
kondoExposed_mediumValue = 20000
kondoExposed_highValue = 50000
kondoExposed_insaneValue = 100000
kondoExposed_hideBelowValue = 0
kondoExposed_useLiveGEPrices = true
lowValue = 5000
mediumValue = 20000
highValue = 50000
insaneValue = 100000
hideBelowValue = 0
useLiveGEPrices = true
StoreData("ground-item-tags","");
StoreData("ground-item-ignore","");
StoreData("low-value", kondoExposed_lowValue)
StoreData("ground-item-use-remote", kondoExposed_useLiveGEPrices)
StoreData("medium-value", kondoExposed_mediumValue)
StoreData("high-value", kondoExposed_highValue)
StoreData("insane-value", kondoExposed_insaneValue)
StoreData("hide-below-value", kondoExposed_hideBelowValue)
StoreData("low-value", lowValue)
StoreData("ground-item-use-remote", useLiveGEPrices)
StoreData("medium-value", mediumValue)
StoreData("high-value", highValue)
StoreData("insane-value", insaneValue)
StoreData("hide-below-value", hideBelowValue)
}
private fun displayRanges() {
val low = kondoExposed_lowValue
val medium = kondoExposed_mediumValue
val high = kondoExposed_highValue
val insane = kondoExposed_insaneValue
val hide = kondoExposed_hideBelowValue
val low = lowValue
val medium = mediumValue
val high = highValue
val insane = insaneValue
val hide = hideBelowValue
SendMessage("== Ground Item Config ==")
SendMessage("Low: $low")
@ -260,12 +259,12 @@ open class plugin : Plugin() {
SendMessage("Insane: $insane")
SendMessage("Hide Below: $hide")
SendMessage("-- Ignored Items --")
for(item in kondoExposed_ignoredItems){
for(item in ignoredItems){
val itemDef = ObjTypeList.get(item)
SendMessage("Ignored: ${itemDef.name} ${itemDef.id}")
}
SendMessage("-- Tagged Items --")
for(item in kondoExposed_taggedItems){
for(item in taggedItems){
val itemDef = ObjTypeList.get(item)
SendMessage("Tagged: ${itemDef.name} ${itemDef.id}")
}
@ -278,19 +277,19 @@ open class plugin : Plugin() {
}
fun OnKondoValueUpdated() {
StoreData("ground-item-tags",kondoExposed_taggedItems);
StoreData("ground-item-ignore",kondoExposed_ignoredItems);
StoreData("low-value", kondoExposed_lowValue)
StoreData("medium-value", kondoExposed_mediumValue)
StoreData("high-value", kondoExposed_highValue)
StoreData("insane-value", kondoExposed_insaneValue)
StoreData("ground-item-use-remote", kondoExposed_useLiveGEPrices)
StoreData("hide-below-value", kondoExposed_hideBelowValue)
StoreData("ground-item-tags",taggedItems);
StoreData("ground-item-ignore",ignoredItems);
StoreData("low-value", lowValue)
StoreData("medium-value", mediumValue)
StoreData("high-value", highValue)
StoreData("insane-value", insaneValue)
StoreData("ground-item-use-remote", useLiveGEPrices)
StoreData("hide-below-value", hideBelowValue)
gePriceMap = loadGEPrices();
}
fun loadGEPrices(): Map<String, String> {
return if (kondoExposed_useLiveGEPrices) {
return if (useLiveGEPrices) {
try {
println("GroundItems: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
@ -325,7 +324,7 @@ open class plugin : Plugin() {
} else {
try {
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 ->
val json = lines.joinToString("\n")
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }

View file

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

View file

@ -1,5 +1,6 @@
package KondoKit
import KondoKit.plugin.Companion.WIDGET_COLOR
import java.awt.Canvas
import java.awt.Color
import java.awt.Dimension
@ -8,31 +9,25 @@ import java.awt.image.BufferedImage
class ImageCanvas(private val image: BufferedImage) : Canvas() {
var fillColor: Color = WIDGET_COLOR
init {
// Manually set the alpha value to 255 (fully opaque) only for pixels that are not fully transparent
val width = image.width
val height = image.height
for (y in 0 until height) {
for (x in 0 until width) {
// Retrieve the current pixel color
val color = image.getRGB(x, y)
// Check if the pixel is not fully transparent (i.e., color is not 0)
if (color != 0) {
// Ensure the alpha is set to 255 (fully opaque)
val newColor = (color and 0x00FFFFFF) or (0xFF shl 24)
// Set the pixel with the updated color
image.setRGB(x, y, newColor)
}
}
}
}
override fun paint(g: Graphics) {
super.paint(g)
g.color = Color(27, 27, 27)
g.color = fillColor
g.fillRect(0, 0, width, height)
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
import KondoKit.Helpers.addMouseListenerToAll
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.plugin.Companion.POPUP_BACKGROUND
import KondoKit.plugin.Companion.POPUP_FOREGROUND
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import plugin.api.API
import rt4.NpcTypeList
import rt4.ObjStackNode
import rt4.Player
import rt4.SceneGraph
import rt4.*
import java.awt.*
import java.awt.Font
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage
import java.io.BufferedReader
import java.io.InputStreamReader
@ -22,19 +28,23 @@ import java.net.URL
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import javax.swing.*
import kotlin.math.ceil
object LootTrackerView {
private const val SNAPSHOT_LIFESPAN = 10
const val BAG_ICON = 900;
const val BAG_ICON = 900
val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>()
var gePriceMap = loadGEPrices()
const val VIEW_NAME = "LOOT_TRACKER_VIEW"
private val lootItemPanels = mutableMapOf<String, MutableMap<Int, Int>>()
private val npcKillCounts = mutableMapOf<String, Int>()
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> {
return if (plugin.kondoExposed_useLiveGEPrices) {
return if (plugin.useLiveGEPrices) {
try {
println("LootTracker: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
@ -69,7 +79,7 @@ object LootTrackerView {
} else {
try {
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 ->
val json = lines.joinToString("\n")
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
@ -94,19 +104,37 @@ object LootTrackerView {
fun createLootTrackerView(): JPanel {
return JPanel().apply {
layout = FlowLayout(FlowLayout.CENTER, 0, 5)
fun createLootTrackerView() {
lootTrackerView = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS) // Use BoxLayout on Y axis to stack widgets vertically
background = VIEW_BACKGROUND_COLOR
preferredSize = Dimension(270, 700)
maximumSize = Dimension(270, 700)
minimumSize = Dimension(270, 700)
add(Box.createVerticalStrut(5))
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()
repaint()
if(focusedView == VIEW_NAME)
repaint()
}
}
@ -122,7 +150,7 @@ object LootTrackerView {
totalTrackerWidget?.let {
it.previousXp += newVal
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))
return XPWidget(
skillId = -1,
panel = createWidgetPanel(bufferedImageSprite,l2,l1),
container = createWidgetPanel(bufferedImageSprite,l2,l1),
xpGainedLabel = l2,
xpLeftLabel = JLabel(),
actionsRemainingLabel = JLabel(),
xpPerHourLabel = l1,
progressBar = ProgressBar(0.0, Color(150, 50, 50)),
progressBar = ProgressBar(0.0, Color(0,0,0)), // unused.
totalXpGained = 0,
startTime = System.currentTimeMillis(),
previousXp = 0
@ -146,16 +174,19 @@ object LootTrackerView {
private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): Panel {
val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
size = Dimension(width, height)
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
minimumSize = preferredSize
maximumSize = preferredSize
size = preferredSize
background = WIDGET_COLOR
}
val imageContainer = Panel(FlowLayout()).apply {
val imageContainer = Panel(BorderLayout()).apply {
background = WIDGET_COLOR
add(imageCanvas)
add(imageCanvas, BorderLayout.NORTH)
}
return Panel(BorderLayout(5, 5)).apply {
return Panel(BorderLayout(5, 0)).apply {
background = WIDGET_COLOR
preferredSize = TOTAL_XP_WIDGET_SIZE
add(imageContainer, BorderLayout.WEST)
@ -164,7 +195,7 @@ object LootTrackerView {
}
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
add(l1)
add(l2)
@ -173,7 +204,7 @@ object LootTrackerView {
private fun createLabel(text: String): JLabel {
return JLabel(text).apply {
font = Font("Arial", Font.PLAIN, 11)
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
horizontalAlignment = JLabel.LEFT
}
}
@ -189,12 +220,19 @@ object LootTrackerView {
// Recalculate lootPanel size based on the number of unique items.
val totalItems = lootItemPanels[npcName]?.size ?: 0
val rowsNeeded = Math.ceil(totalItems / 6.0).toInt()
val lootPanelHeight = rowsNeeded * 36 + (rowsNeeded - 1)
lootPanel.preferredSize = Dimension(270, lootPanelHeight+10)
val rowsNeeded = ceil(totalItems / 6.0).toInt()
val lootPanelHeight = rowsNeeded * (40)
val size = Dimension(lootPanel.width,lootPanelHeight+32)
lootPanel.parent.preferredSize = size
lootPanel.parent.minimumSize = size
lootPanel.parent.maximumSize = size
lootPanel.parent.revalidate()
lootPanel.parent.repaint()
lootPanel.revalidate()
lootPanel.repaint()
if(focusedView == VIEW_NAME)
lootPanel.repaint()
}
}
@ -204,27 +242,93 @@ object LootTrackerView {
}
private fun createItemPanel(itemId: Int, quantity: Int): JPanel {
val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 0, 0))
return FixedSizePanel(Dimension(36, 32)).apply {
val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 1, 3153952))
// Create the panel for the item
val itemPanel = FixedSizePanel(Dimension(36, 32)).apply {
preferredSize = Dimension(36, 32)
background = WIDGET_COLOR
minimumSize = preferredSize
maximumSize = preferredSize
add(ImageCanvas(bufferedImageSprite).apply {
val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
preferredSize = Dimension(36, 32)
background = WIDGET_COLOR
minimumSize = 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)
// 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) {
panel.removeAll()
panel.add(createItemPanel(itemId, quantity).components[0], BorderLayout.CENTER)
panel.revalidate()
panel.repaint()
if(focusedView == VIEW_NAME)
panel.repaint()
}
private fun updateKillCountLabel(lootTrackerPanel: JPanel, npcName: String) {
@ -246,9 +350,11 @@ object LootTrackerView {
?.apply {
val newValue = (getClientProperty("val") as? Int ?: 0) + valueOfNewDrops.toInt()
text = "${formatValue(newValue)} gp"
foreground = primaryColor
putClientProperty("val", newValue)
revalidate()
repaint()
if(focusedView == VIEW_NAME)
repaint()
}
}
}
@ -276,7 +382,7 @@ object LootTrackerView {
if (newDrops.isNotEmpty()) {
val npcName = NpcTypeList.get(npcId).name
lastConfirmedKillNpcId = npcId;
lastConfirmedKillNpcId = npcId
handleNewDrops(npcName.toString(), newDrops, lootTrackerView)
toRemove.add(npcId)
} else if (snapshot.age >= SNAPSHOT_LIFESPAN) {
@ -308,11 +414,11 @@ object LootTrackerView {
private fun handleNewDrops(npcName: String, newDrops: Set<Item>, lootTrackerView: JPanel) {
findLootItemsPanel(lootTrackerView, npcName)?.let {
} ?: run {
// Panel doesn't exist, so create and add it
lootTrackerView.add(createLootFrame(npcName))
lootTrackerView.add(Helpers.Spacer(height = 15))
lootTrackerView.add(Box.createVerticalStrut(8))
lootTrackerView.revalidate()
lootTrackerView.repaint()
if(focusedView == VIEW_NAME)
lootTrackerView.repaint()
}
npcKillCounts[npcName] = npcKillCounts.getOrDefault(npcName, 0) + 1
@ -321,7 +427,7 @@ object LootTrackerView {
newDrops.forEach { drop ->
val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity
updateValueLabel(lootTrackerView, geValue.toString(), npcName)
addItemToLootPanel(lootTrackerView, drop, npcName)
plugin.registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
updateTotalValue(geValue)
}
}
@ -330,29 +436,30 @@ object LootTrackerView {
val childFramePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR
minimumSize = Dimension(270, 0)
maximumSize = Dimension(270, 700)
minimumSize = Dimension(230, 0)
maximumSize = Dimension(230, 700)
name = "HELLO_WORLD"
}
val labelPanel = JPanel(BorderLayout()).apply {
background = Color(21, 21, 21)
background = TITLE_BAR_COLOR
border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
maximumSize = Dimension(270, 24)
maximumSize = Dimension(230, 24)
minimumSize = maximumSize
preferredSize = maximumSize
}
val killCount = npcKillCounts.getOrPut(npcName) { 0 }
val countLabel = JLabel(formatHtmlLabelText(npcName, secondaryColor, " x $killCount", primaryColor)).apply {
foreground = Color(200, 200, 200)
font = Font("Arial", Font.PLAIN, 12)
foreground = secondaryColor
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
horizontalAlignment = JLabel.LEFT
name = "killCountLabel_$npcName"
}
val valueLabel = JLabel("0 gp").apply {
foreground = Color(200, 200, 200)
font = Font("Arial", Font.PLAIN, 12)
foreground = secondaryColor
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
horizontalAlignment = JLabel.RIGHT
name = "valueLabel_$npcName"
}
@ -370,19 +477,120 @@ object LootTrackerView {
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(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
}
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() {
override fun getPreferredSize(): Dimension {

View file

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

View file

@ -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.SoftwareIndexedSprite
import rt4.SoftwareSprite
import java.awt.Color
import java.awt.image.BufferedImage
// Define interfaces for common sprite types
interface BaseSprite {
val width: Int
val height: Int
}
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 {
/**
* 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 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.
*/
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 height = sprite.height
val pixels = sprite.pixels
// Create a BufferedImage with ARGB color model
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
// 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)
when (sprite) {
is IndexedSprite -> {
val pixels = sprite.pixels
val palette = sprite.palette
// 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
}
fun convertToBufferedImage(sprite: SoftwareIndexedSprite): BufferedImage {
val width = sprite.width
val height = sprite.height
val pixels = sprite.pixels // byte[]
val palette = sprite.pallet
// Create a BufferedImage with ARGB color model
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
// Manually set pixels using the palette
for (y in 0 until height) {
for (x in 0 until width) {
// 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
/**
* Applies a tint to a given color using the tint's alpha value to control the intensity.
*
* @param original The original color.
* @param tint The tint color to be applied.
* @param brightnessBoost A multiplier to boost the brightness of the image.
* @return The tinted color.
*/
private fun applyTint(original: Color, tint: Color, brightnessBoost: Float): Color {
val boostedColor = applyBrightness(original, brightnessBoost)
val r = (boostedColor.red * tint.red / 255).coerceIn(0, 255)
val g = (boostedColor.green * tint.green / 255).coerceIn(0, 255)
val b = (boostedColor.blue * tint.blue / 255).coerceIn(0, 255)
return Color(r, g, b, boostedColor.alpha)
}
fun convertToBufferedImage(sprite: GlIndexedSprite): BufferedImage {
val width = sprite.width
val height = sprite.height
val pixels = sprite.pixels // byte[]
val palette = sprite.pallet
// Create a BufferedImage with ARGB color model
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
// Manually set pixels using the palette
for (y in 0 until height) {
for (x in 0 until width) {
// 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
/**
* Boosts the brightness of a given color.
*
* @param original The original color.
* @param factor The multiplier to boost the brightness.
* @return The color with boosted brightness.
*/
private fun applyBrightness(original: Color, factor: Float): Color {
val r = (original.red * factor).coerceIn(0.0f, 255.0f).toInt()
val g = (original.green * factor).coerceIn(0.0f, 255.0f).toInt()
val b = (original.blue * factor).coerceIn(0.0f, 255.0f).toInt()
return Color(r, g, b, original.alpha)
}
fun convertToBufferedImage(sprite: GlSprite): BufferedImage {
val width = sprite.width
val height = sprite.height
val pixels = sprite.pixels
// Create a BufferedImage with ARGB color model
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
if(pixels == null) {
return image
}
// 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
/**
* Converts a color to grayscale and applies a brightness boost.
*
* @param original The original color.
* @param brightnessBoost A multiplier to boost the brightness.
* @return The grayscale version of the color with boosted brightness.
*/
private fun applyGrayscale(original: Color, brightnessBoost: Float): Color {
// Calculate the grayscale value using the luminosity method
val grayValue = (0.3 * original.red + 0.59 * original.green + 0.11 * original.blue).toInt()
val boostedGray = (grayValue * brightnessBoost).coerceIn(0.0f, 255.0f).toInt()
return Color(boostedGray, boostedGray, boostedGray, original.alpha)
}
fun getBufferedImageFromSprite(sprite: Any?) : BufferedImage {
return when(sprite){
is GlSprite -> convertToBufferedImage(sprite)
is SoftwareSprite -> convertToBufferedImage(sprite)
is SoftwareIndexedSprite -> convertToBufferedImage(sprite)
is GlIndexedSprite -> convertToBufferedImage(sprite)
else -> BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
/**
* Converts an unknown sprite object into a BufferedImage if it matches a known sprite type.
*
* @param sprite The sprite object 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 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 {
const val MAX_LEVEL = 99
const val INVALID_LEVEL = -1
const val SKILLS_XP_TABLE = 716
private const val MAX_LEVEL = 99
private const val INVALID_LEVEL = -1
private const val SKILLS_XP_TABLE = 716
private var xpTable: MutableList<Int> = mutableListOf()

View file

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

View file

@ -4,214 +4,201 @@ import KondoKit.Constants.COMBAT_LVL_SPRITE
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.Helpers.formatNumber
import KondoKit.Helpers.getSpriteId
import KondoKit.Helpers.showAlert
import KondoKit.HiscoresView.createHiscoreSearchView
import KondoKit.HiscoresView.hiScoreView
import KondoKit.LootTrackerView.BAG_ICON
import KondoKit.LootTrackerView.createLootTrackerView
import KondoKit.LootTrackerView.lootTrackerView
import KondoKit.LootTrackerView.npcDeathSnapshots
import KondoKit.LootTrackerView.onPostClientTick
import KondoKit.LootTrackerView.takeGroundSnapshot
import KondoKit.ReflectiveEditorView.addPlugins
import KondoKit.ReflectiveEditorView.createReflectiveEditorView
import KondoKit.ReflectiveEditorView.reflectiveEditorView
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.XPTrackerView.createTotalXPWidget
import KondoKit.Themes.Theme
import KondoKit.Themes.ThemeType
import KondoKit.Themes.getTheme
import KondoKit.XPTrackerView.createXPTrackerView
import KondoKit.XPTrackerView.createXPWidget
import KondoKit.XPTrackerView.initialXP
import KondoKit.XPTrackerView.resetXPTracker
import KondoKit.XPTrackerView.totalXPWidget
import KondoKit.XPTrackerView.updateWidget
import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.plugin.StateManager.initialXP
import KondoKit.plugin.StateManager.totalXPWidget
import KondoKit.plugin.StateManager.xpWidgets
import KondoKit.XPTrackerView.xpTrackerView
import KondoKit.XPTrackerView.xpWidgets
import KondoKit.plugin.StateManager.focusedView
import plugin.Plugin
import plugin.api.*
import plugin.api.API.*
import plugin.api.FontColor.fromColor
import rt4.GameShell
import rt4.*
import rt4.DisplayMode
import rt4.GameShell.canvas
import rt4.GameShell.frame
import rt4.GlRenderer
import rt4.InterfaceList
import rt4.Player
import rt4.client.js5Archive8
import rt4.client.mainLoadState
import java.awt.*
import java.awt.Font
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Exposed(val description: String = "")
class plugin : Plugin() {
companion object {
val WIDGET_SIZE = Dimension(270, 55)
val TOTAL_XP_WIDGET_SIZE = Dimension(270, 30)
val IMAGE_SIZE = Dimension(20, 20)
val WIDGET_COLOR = Color(27, 27, 27)
val VIEW_BACKGROUND_COLOR = Color(37, 37, 37)
val primaryColor = Color(129, 129, 129) // Color for "XP Gained:"
val secondaryColor = Color(226, 226, 226) // Color for "0"
var kondoExposed_useLiveGEPrices = true
var kondoExposed_playerXPMultiplier = 5
const val FIXED_WIDTH = 782
const val SCROLLPANE_WIDTH = 340
val WIDGET_SIZE = Dimension(220, 50)
val TOTAL_XP_WIDGET_SIZE = Dimension(220, 30)
val IMAGE_SIZE = Dimension(25, 23)
// Default Theme Colors
var WIDGET_COLOR = Color(30, 30, 30)
var TITLE_BAR_COLOR = Color(21, 21, 21)
var VIEW_BACKGROUND_COLOR = Color(40, 40, 40)
var primaryColor = Color(165, 165, 165) // Color for "XP Gained:"
var secondaryColor = Color(255, 255, 255) // Color for "0"
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 LVL_ICON = 898
private const val LOOT_ICON = 777
private const val MAG_SPRITE = 1423
const val LVL_ICON = 898
private lateinit var cardLayout: CardLayout
private lateinit var mainContentPanel: Panel
private var scrollPane: JScrollPane? = null
private var hiScoreView: JPanel? = null
private var reflectiveEditorView: JPanel? = null
private var lootTrackerView: JPanel? = null
private var xpTrackerView: JPanel? = null
private lateinit var mainContentPanel: JPanel
private var rightPanelWrapper: JScrollPane? = null
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 loginScreen = 160;
private var loginScreen = 160
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 {
// 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;
override fun Init() {
// Disable Font AA
System.setProperty("sun.java2d.opengl", "false")
System.setProperty("awt.useSystemAAFontSettings", "off")
System.setProperty("swing.aatext", "false")
}
override fun OnLogin() {
if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) {
// if we logged in with a new character
// we need to reset the trackers
xpTrackerView?.removeAll()
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()
xpTrackerView?.let { resetXPTracker(it) }
}
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>?) {
if (currentEntries != null) {
for ((index, entry) in currentEntries.withIndex()) {
if (entry.type == MiniMenuType.PLAYER && index == currentEntries.size - 1) {
val input = entry.subject
val username = input
.replace(Regex("<col=[0-9a-fA-F]{6}>"), "")
.replace(Regex("<img=\\d+>"), "")
.split(" ") // Split by spaces
.first() // Take the first part, which is the username
InsertMiniMenuEntry("Lookup", entry.subject, searchHiscore(username.replace(" ","_")))
// Trim spaces, clean up tags, and remove the level info
val cleanedInput = input
.trim() // Remove any leading/trailing spaces
.replace(Regex("<col=[0-9a-fA-F]{6}>"), "") // Remove color tags
.replace(Regex("<img=\\d+>"), "") // Remove image tags
.replace(Regex("\\(level: \\d+\\)"), "") // Remove level text e.g. (level: 44)
.trim() // Trim again to remove extra spaces after removing level text
// Proceed with the full cleaned username
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 {
if (!initialized) return true
UpdateDisplaySettings()
frame.remove(scrollPane)
updateDisplaySettings()
frame.remove(rightPanelWrapper)
frame.layout = BorderLayout()
frame.add(scrollPane, BorderLayout.EAST)
// Clear or regenerate the reflectiveEditorView
reflectiveEditorView?.removeAll()
reflectiveEditorView?.revalidate()
if(StateManager.focusedView == "REFLECTIVE_EDITOR_VIEW")
reflectiveEditorView?.repaint()
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
frame.revalidate()
frame.repaint()
pluginsReloaded = true
reloadInterfaces = true
return true
}
override fun OnXPUpdate(skillId: Int, xp: Int) {
if (!initialXP.containsKey(skillId)) {
initialXP[skillId] = xp
return
}
if (!initialXP.containsKey(skillId)) {
initialXP[skillId] = xp
return
}
var xpWidget = xpWidgets[skillId]
if (xpWidget != null) {
updateWidget(xpWidget, xp)
} else {
val previousXp = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
var xpWidget = xpWidgets[skillId]
xpWidget = createXPWidget(skillId, previousXp)
xpWidgets[skillId] = xpWidget
if (xpWidget != null) {
updateWidget(xpWidget, xp)
} else {
val previousXp = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
xpTrackerView?.add(wrappedWidget(xpWidget.container))
xpTrackerView?.add(Box.createVerticalStrut(5))
xpWidget = createXPWidget(skillId, previousXp)
xpWidgets[skillId] = xpWidget
if(focusedView == XPTrackerView.VIEW_NAME) {
xpTrackerView?.revalidate()
xpTrackerView?.repaint()
}
xpTrackerView?.add(wrappedWidget(xpWidget.panel))
xpTrackerView?.add(Box.createVerticalStrut(5))
xpTrackerView?.revalidate()
if(StateManager.focusedView == "XP_TRACKER_VIEW")
xpTrackerView?.repaint()
updateWidget(xpWidget, xp)
}
updateWidget(xpWidget, xp)
}
}
override fun Draw(timeDelta: Long) {
@ -221,155 +208,498 @@ class plugin : Plugin() {
}
if (pluginsReloaded) {
InterfaceList.method3712(true) // Gets the resize working correctly
reflectiveEditorView?.let { addPlugins(it) }
pluginsReloaded = false
}
if (reloadInterfaces){
InterfaceList.method3712(true) // Gets the resize working correctly
reloadInterfaces = false
}
accumulatedTime += timeDelta
if (accumulatedTime >= tickInterval) {
if (accumulatedTime >= TICK_INTERVAL) {
lootTrackerView?.let { onPostClientTick(it) }
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
if(!initialized && mainLoadState >= loginScreen) {
initKondoUI()
}
}
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) {
kondoExposed_useLiveGEPrices = (GetData("kondoUseRemoteGE") as? Boolean) ?: true
kondoExposed_playerXPMultiplier = (GetData("kondoPlayerXPMultiplier") as? Int) ?: 5
cardLayout = CardLayout()
mainContentPanel = Panel(cardLayout)
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()
override fun LateDraw(timeDelta: Long) {
if (!initialized) return
if(GameShell.fullScreenFrame != null) {
DisplayMode.setWindowMode(true, 0, FIXED_WIDTH, FIXED_HEIGHT)
showAlert("Fullscreen is not supported by KondoKit. Disable the plugin first.",
"Error",
JOptionPane.INFORMATION_MESSAGE
)
return
}
if(!useScaledFixed) return
if(GetWindowMode() == WindowMode.FIXED){
moveAltCanvasToFront()
} else {
moveCanvasToFront()
}
altCanvas?.updateGameImage() // Update the game image as needed
}
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 xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedXpPerHour = formatNumber(xpPerHour)
xpWidget.xpPerHourLabel.text =
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 totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
totalXPWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
totalXPWidget.panel.repaint()
totalXPWidget.container.repaint()
}
}
override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) {
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
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 {
val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId))
val buttonSize = Dimension(42, 42)
val imageSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
private fun updateDisplaySettings() {
val mode = GetWindowMode()
val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH
lastUIOffset = uiOffset
val actionListener = ActionListener {
cardLayout.show(mainContentPanel, viewName)
StateManager.focusedView = viewName
if(mode != WindowMode.FIXED) {
destroyAltCanvas()
} else if (useScaledFixed && altCanvas == null) {
initAltCanvas()
}
when (mode) {
WindowMode.FIXED -> {
if (frame.width < FIXED_WIDTH + currentScrollPaneWidth + uiOffset) {
frame.setSize(FIXED_WIDTH + currentScrollPaneWidth + uiOffset, frame.height)
}
val difference = frame.width - (uiOffset + currentScrollPaneWidth)
if (useScaledFixed) {
GameShell.leftMargin = 0
val canvasWidth = difference + uiOffset / 2
val canvasHeight = frame.height - canvas.y // Restricting height to frame height
altCanvas?.size = Dimension(canvasWidth, canvasHeight)
altCanvas?.setLocation(0, canvas.y)
canvas.setLocation(0, canvas.y)
} else {
val difference = frame.width - (FIXED_WIDTH + uiOffset + currentScrollPaneWidth)
GameShell.leftMargin = difference / 2
}
}
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(lastUIOffset != uiOffset){
reloadInterfaces = true
}
updateDisplaySettings()
}
private fun initAltCanvas(){
if(GetWindowMode() != WindowMode.FIXED || altCanvas != null) return
if (frame != null) {
altCanvas = AltCanvas().apply {
preferredSize = Dimension(FIXED_WIDTH, FIXED_HEIGHT)
}
altCanvas?.let { frame.add(it) }
moveAltCanvasToFront()
frame.setComponentZOrder(rightPanelWrapper, 2)
}
}
private fun destroyAltCanvas(){
if (altCanvas == null) return
moveCanvasToFront()
frame.remove(altCanvas)
altCanvas = null
}
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 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 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)
}
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 {
background = WIDGET_COLOR
preferredSize = imageSize
maximumSize = 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()
preferredSize = buttonSize
maximumSize = buttonSize
minimumSize = buttonSize
background = WIDGET_COLOR
isFocusPainted = false
isBorderPainted = false
isOpaque = true
val gbc = GridBagConstraints().apply {
anchor = GridBagConstraints.CENTER
fill = GridBagConstraints.NONE // Prevents stretching
}
add(imageCanvas, gbc)
addActionListener(actionListener)
add(imageCanvasWrapper, gbc)
// 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 {
val initialXP: MutableMap<Int, Int> = HashMap()
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
var totalXPWidget: XPWidget? = null
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'
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 initTime: Long = 0
private var logoutFlag = true
private var displayMessageCounter = 0
private var component: Component? = null
@ -35,6 +36,20 @@ class plugin : Plugin() {
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) {
if (component == null)
return
@ -134,6 +149,9 @@ class plugin : Plugin() {
API.InsertMiniMenuEntry("Disable Timer", "") {
timeMode = DEFAULT_TIME_MODE
}
API.InsertMiniMenuEntry("Reset Play Time", "") {
initTime = System.currentTimeMillis()
}
}
}
}

View file

@ -1,3 +1,3 @@
AUTHOR='Woahscam, Ceikry'
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,81 +1,89 @@
package ToggleResizableSD
import KondoKit.Exposed
import plugin.Plugin
import plugin.annotations.PluginMeta
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.KeyEvent
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() {
API.AddKeyboardListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_F12) {
toggleResize = true
toggleResizableSd()
}
}
})
if (!DisplayMode.resizableSD && API.GetData("use-resizable-sd") == true) {
toggleResize = true
useResizable = DisplayMode.resizableSD
if (API.GetData("use-resizable-sd") == true) {
useResizable = true
}
var osNameLowerCase: String = ""
var osName: String
try {
osName = System.getProperty("os.name")
} catch (e: Exception) {
osName = "Unknown"
}
osNameLowerCase = osName.toLowerCase()
var osNameLowerCase: String = System.getProperty("os.name").toLowerCase()
if (!osNameLowerCase.startsWith("mac")) {
wantHd = true
}
if (API.GetData("want-hd") == false) {
wantHd = false
}
}
override fun ProcessCommand(commandStr: String, args: Array<out String>?) {
when(commandStr.toLowerCase()) {
when (commandStr.toLowerCase()) {
"::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
API.StoreData("want-hd", wantHd)
StoreData("want-hd", wantHd)
API.SendMessage("You have turned login screen HD " + (if (wantHd) "on" else "off"))
}
}
}
fun toggleResizableSd() {
//We only want to toggle resizable SD when we are logged in and the lobby/welcome interface is not open.
if (InterfaceList.aClass13_26 == null || client.gameState != 30) {
return
}
toggleResize = false
DisplayMode.resizableSD = !DisplayMode.resizableSD;
if(!DisplayMode.resizableSD){
//Revert to fixed
API.StoreData("use-resizable-sd", false) //Note: It is important to call StoreData before setWindowMode because setWindowMode causes all plugins to reload.
DisplayMode.resizableSD = !DisplayMode.resizableSD
useResizable = DisplayMode.resizableSD
StoreData("use-resizable-sd", useResizable)
if (!DisplayMode.resizableSD) {
DisplayMode.setWindowMode(true, 0, -1, -1)
} 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)
}
}
override fun Draw(timeDelta: Long) {
if (toggleResize) {
if (useResizable != DisplayMode.resizableSD) {
toggleResizableSd()
}
}
fun OnKondoValueUpdated() {
StoreData("want-hd", wantHd)
StoreData("use-resizable-sd", useResizable)
}
override fun OnLogout() {
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.

View file

@ -1,3 +1,3 @@
AUTHOR='ipkpjersi'
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.annotations.PluginMeta
import plugin.api.*
import plugin.api.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.image.BufferedImage
import java.io.InputStream
import javax.imageio.ImageIO
import kotlin.math.ceil
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 drawStart = 175
private val drawPadding = 25
@ -15,41 +37,50 @@ class plugin : Plugin() {
private var totalXp = 0
private val activeGains = ArrayList<XPGain>()
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) {
if (System.currentTimeMillis() - lastGain >= displayTimeout && activeGains.isEmpty())
return
if (shouldSkipDrawing()) return
drawTotalXPBox()
val removeList = ArrayList<XPGain>()
var posX = API.GetWindowDimensions().width / 2
if (API.GetWindowMode() == WindowMode.FIXED)
posX += 60
val movementSpeedFactor = deltaTime / 16.666 // 60 FPS
for(gain in activeGains) {
gain.currentPos -= ceil(deltaTime / 20.0).toInt()
for (gain in activeGains) {
gain.currentPos -= ceil(movementSpeedFactor).toInt() // Adjust movement based on deltaTime
if (gain.currentPos <= drawClear) {
removeList.add(gain)
totalXp += gain.xp
} else if (gain.currentPos <= drawStart){
val sprite = XPSprites.getSpriteForSkill(skillId = gain.skill)
sprite?.render(posX - 25, gain.currentPos - 20)
API.DrawText(
FontType.SMALL,
FontColor.fromColor(Color.WHITE),
TextModifier.LEFT,
addCommas(gain.xp.toString()),
posX,
gain.currentPos
)
} else if (gain.currentPos <= drawStart) {
drawXPDrops(gain)
}
}
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) {
if (xp == lastXp[skill]) return
@ -81,6 +112,54 @@ class plugin : Plugin() {
}
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
val posY = API.GetWindowDimensions().height / 4
@ -89,13 +168,13 @@ class plugin : Plugin() {
API.ClipRect(0, 0, posX * 2, posY * 4)
val horizontal = API.GetSprite(822)
val horizontalTop = API.GetSprite(820)
val tlCorner = API.GetSprite(824)
val blCorner = API.GetSprite(826)
val trCorner = API.GetSprite(825)
val brCorner = API.GetSprite(827)
val bg = API.GetSprite(657)
val horizontal = spriteCache.getOrPut(822) { API.GetSprite(822) }
val horizontalTop = spriteCache.getOrPut(820) { API.GetSprite(820) }
val tlCorner = spriteCache.getOrPut(824) { API.GetSprite(824) }
val blCorner = spriteCache.getOrPut(826) { API.GetSprite(826) }
val trCorner = spriteCache.getOrPut(825) { API.GetSprite(825) }
val brCorner = spriteCache.getOrPut(827) { API.GetSprite(827) }
val bg = spriteCache.getOrPut(657) { API.GetSprite(657) }
bg?.render(posX - 77, 10)
API.FillRect(posX - 75, 5, 140, 30, 0, 64)
@ -112,26 +191,58 @@ class plugin : Plugin() {
horizontalTop?.render(posX + 9, -8)
horizontal?.render(posX + 9, 22)
API.DrawText(
FontType.SMALL,
FontColor.fromColor(Color.WHITE),
TextModifier.LEFT,
"Total Xp: ${addCommas(totalXp.toString())}",
posX - 65,
28
DrawText(
FontType.SMALL,
fromColor(Color.WHITE),
TextModifier.LEFT,
"Total Xp: ${addCommas(totalXp.toString())}",
posX - 65,
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)
fun addCommas(num: String): String{
fun addCommas(num: String): String {
var newString = ""
if(num.length > 9){
if (num.length > 9) {
return "Lots!"
}
var counter = 1
num.reversed().forEach {
if(counter % 3 == 0 && counter != num.length){
if (counter % 3 == 0 && counter != num.length) {
newString += "$it,"
} else {
newString += it
@ -140,4 +251,10 @@ class plugin : Plugin() {
}
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'
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