Merge branch 'Kondo-Kit-Plugin' into 'master'

KondoKit v1.0 Plugin

See merge request 2009scape/rt4-client!23
This commit is contained in:
Ceikry 2024-09-14 18:41:14 +00:00
commit dd52e7e9fb
20 changed files with 133161 additions and 77 deletions

View file

@ -92,11 +92,32 @@ public abstract class Plugin {
*/
public void OnLogin() {}
/**
* Called when an NPC is killed.
*
* @param npcID the unique identifier of the NPC
* @param x the x-coordinate where the NPC died
* @param z the z-coordinate where the NPC died
*/
public void OnKillingBlowNPC(int npcID, int x, int z) {}
/**
* OnLogout is called when the client logs out. This should be used to clear player-relevant plugin state.
*/
public void OnLogout() {}
/**
* Called when the client attempts to reload plugins.
* Implement this method to control the behavior of the plugin during a reload.
*
* @return {@code false} to allow the plugin to be reloaded.
* {@code true} to prevent the plugin from being reloaded and stay loaded,
*/
public boolean OnPluginsReloaded() {
return false;
}
/**
* DrawMiniMenu is called when a MiniMenu entry has been created.
* @param entry the entry

View file

@ -43,7 +43,23 @@ public class PluginRepository {
API.registeredWheelListeners.clear();
API.registeredMouseListeners.clear();
API.registeredKeyListeners.clear();
HashMap<PluginInfo, Plugin> pluginsToKeep = new HashMap<>();
// Check and store plugins with OnPluginsReloaded method
loadedPlugins.forEach((info, plugin) -> {
try {
boolean keep = plugin.OnPluginsReloaded();
if (keep) {
pluginsToKeep.put(info, plugin);
}
} catch (Exception e) {
e.printStackTrace();
}
});
loadedPlugins.clear();
loadedPlugins.putAll(pluginsToKeep);
SaveStorage();
Init();
}
@ -98,6 +114,11 @@ public class PluginRepository {
continue;
}
if (loadedPlugins.containsKey(info)) {
System.out.println("Skipping reloading of plugin " + file.getName() + " as it already exists and has OnPluginsReloaded.");
continue;
}
try {
Plugin thisPlugin = (Plugin) clazz.newInstance();
thisPlugin._init();
@ -179,6 +200,10 @@ public class PluginRepository {
loadedPlugins.values().forEach((plugin) -> plugin.OnLogin());
}
public static void OnKillingBlowNPC(int npcId, int x, int z) {
loadedPlugins.values().forEach((plugin) -> plugin.OnKillingBlowNPC(npcId, x, z));
}
public static void SaveStorage() {
if (pluginStorage.containsKey("_keystoreDirty")) {
pluginStorage.remove("_keystoreDirty");

View file

@ -32,6 +32,10 @@ public final class GlIndexedSprite extends IndexedSprite {
@OriginalMember(owner = "client!oh", name = "q", descriptor = "I")
private int anInt4284 = 0;
public byte[] pixels;
public int[] pallet;
@OriginalMember(owner = "client!oh", name = "<init>", descriptor = "(IIIIII[B[I)V")
public GlIndexedSprite(@OriginalArg(0) int arg0, @OriginalArg(1) int arg1, @OriginalArg(2) int arg2, @OriginalArg(3) int arg3, @OriginalArg(4) int arg4, @OriginalArg(5) int arg5, @OriginalArg(6) byte[] arg6, @OriginalArg(7) int[] arg7) {
this.innerWidth = arg0;
@ -40,6 +44,8 @@ public final class GlIndexedSprite extends IndexedSprite {
this.yOffset = arg3;
this.width = arg4;
this.height = arg5;
this.pixels = arg6;
this.pallet = arg7;
this.method3337(arg6, arg7);
this.method3339();
}

View file

@ -26,6 +26,8 @@ public class GlSprite extends Sprite {
@OriginalMember(owner = "client!cf", name = "ab", descriptor = "I")
public int textureId = -1;
public int[] pixels;
@OriginalMember(owner = "client!cf", name = "Z", descriptor = "I")
private int anInt1871 = -1;
@ -40,6 +42,7 @@ public class GlSprite extends Sprite {
this.anInt1861 = arg3;
this.width = arg4;
this.height = arg5;
this.pixels = arg6;
this.method1430(arg6);
this.method1431();
}
@ -52,6 +55,7 @@ public class GlSprite extends Sprite {
this.anInt1861 = arg0.anInt1861;
this.width = arg0.width;
this.height = arg0.height;
this.pixels = arg0.pixels;
this.method1430(arg0.pixels);
this.method1431();
}

View file

@ -3034,6 +3034,7 @@ public class Protocol {
boolean isKillingBlow = (local18 & 0x80) != 0;
if (isKillingBlow) {
PluginRepository.OnKillingBlowNPC(npc.type.id,npc.movementQueueX[0],npc.movementQueueZ[0]);
local43 = inboundBuffer.g2add();
if (local43 == 65535) {
local43 = -1;

View file

@ -12,7 +12,7 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
public byte[] pixels;
@OriginalMember(owner = "client!ek", name = "n", descriptor = "[I")
private final int[] anIntArray144;
public final int[] pallet;
@OriginalMember(owner = "client!ek", name = "<init>", descriptor = "(IIIIII[B[I)V")
public SoftwareIndexedSprite(@OriginalArg(0) int arg0, @OriginalArg(1) int arg1, @OriginalArg(2) int arg2, @OriginalArg(3) int arg3, @OriginalArg(4) int arg4, @OriginalArg(5) int arg5, @OriginalArg(6) byte[] arg6, @OriginalArg(7) int[] arg7) {
@ -23,7 +23,7 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
this.width = arg4;
this.height = arg5;
this.pixels = arg6;
this.anIntArray144 = arg7;
this.pallet = arg7;
}
@OriginalMember(owner = "client!ek", name = "<init>", descriptor = "(III)V")
@ -32,7 +32,7 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
this.innerHeight = this.height = arg1;
this.xOffset = this.yOffset = 0;
this.pixels = new byte[arg0 * arg1];
this.anIntArray144 = new int[arg2];
this.pallet = new int[arg2];
}
@OriginalMember(owner = "client!ek", name = "a", descriptor = "([I[B[IIIIIIIIII)V")
@ -149,29 +149,29 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
@OriginalMember(owner = "client!ek", name = "b", descriptor = "(III)V")
public final void adjustPalette(@OriginalArg(0) int arg0, @OriginalArg(1) int arg1, @OriginalArg(2) int arg2) {
for (@Pc(1) int local1 = 0; local1 < this.anIntArray144.length; local1++) {
@Pc(15) int local15 = this.anIntArray144[local1] >> 16 & 0xFF;
for (@Pc(1) int local1 = 0; local1 < this.pallet.length; local1++) {
@Pc(15) int local15 = this.pallet[local1] >> 16 & 0xFF;
local15 += arg0;
if (local15 < 0) {
local15 = 0;
} else if (local15 > 255) {
local15 = 255;
}
@Pc(38) int local38 = this.anIntArray144[local1] >> 8 & 0xFF;
@Pc(38) int local38 = this.pallet[local1] >> 8 & 0xFF;
local38 += arg1;
if (local38 < 0) {
local38 = 0;
} else if (local38 > 255) {
local38 = 255;
}
@Pc(59) int local59 = this.anIntArray144[local1] & 0xFF;
@Pc(59) int local59 = this.pallet[local1] & 0xFF;
local59 += arg2;
if (local59 < 0) {
local59 = 0;
} else if (local59 > 255) {
local59 = 255;
}
this.anIntArray144[local1] = (local15 << 16) + (local38 << 8) + local59;
this.pallet[local1] = (local15 << 16) + (local38 << 8) + local59;
}
}
@ -226,7 +226,7 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
local7 += local21 * local145;
local125 += local145;
}
method1394(SoftwareRaster.pixels, this.pixels, this.anIntArray144, local7, local9, local41, local125, arg2, arg3, local21, local27, local2, arg4);
method1394(SoftwareRaster.pixels, this.pixels, this.pallet, local7, local9, local41, local125, arg2, arg3, local21, local27, local2, arg4);
}
@OriginalMember(owner = "client!ek", name = "a", descriptor = "()V")
@ -309,7 +309,7 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
local27 += local36;
}
if (local23 > 0 && local20 > 0) {
method1397(SoftwareRaster.pixels, this.pixels, this.anIntArray144, local17, local15, local23, local20, local27, local29, arg2);
method1397(SoftwareRaster.pixels, this.pixels, this.pallet, local17, local15, local23, local20, local27, local29, arg2);
}
}
@ -383,7 +383,7 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
local7 += local21 * local145;
local125 += local145;
}
method1391(SoftwareRaster.pixels, this.pixels, this.anIntArray144, local7, local9, local41, local125, arg2, arg3, local21, local27, local2);
method1391(SoftwareRaster.pixels, this.pixels, this.pallet, local7, local9, local41, local125, arg2, arg3, local21, local27, local2);
}
@OriginalMember(owner = "client!ek", name = "a", descriptor = "(II)V")
@ -424,7 +424,7 @@ public final class SoftwareIndexedSprite extends IndexedSprite {
local27 += local36;
}
if (local23 > 0 && local20 > 0) {
method1393(SoftwareRaster.pixels, this.pixels, this.anIntArray144, local17, local15, local23, local20, local27, local29);
method1393(SoftwareRaster.pixels, this.pixels, this.pallet, local17, local15, local23, local20, local27, local29);
}
}
}

View file

@ -350,15 +350,15 @@ public final class client extends GameShell {
@OriginalMember(owner = "client!pl", name = "a", descriptor = "(II)V")
public static void setGameState(@OriginalArg(0) int arg0) {
if(arg0 == 30) {
PluginRepository.OnLogin();
}
if (gameState == arg0) {
return;
}
if (gameState == 0) {
LoadingBarAwt.clear();
}
if (gameState == 30) {
PluginRepository.OnLogin();
}
if (arg0 == 40) {
LoginManager.clear();
}

View file

@ -12,6 +12,8 @@ import rt4.*
import java.awt.Color
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import kotlin.math.roundToInt
@ -24,24 +26,21 @@ import kotlin.math.roundToInt
cmds ::set(low,med,high,insane,hide), ::(tag,ignore)item ID, ::(reset)groundconfig
Special thanks to Chisato for the original skeleton.
""",
version = 1.1
version = 1.2
)
open class plugin : Plugin() {
private var lowValue = 5000
private var mediumValue = 20000
private var highValue = 50000
private var insaneValue = 100000
private var hideBelowValue = 0
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 taggedItems: List<Int>
private lateinit var ignoredItems: List<Int>
private lateinit var kondoExposed_taggedItems: List<Int>
private lateinit var kondoExposed_ignoredItems: List<Int>
private val gePriceMap = try {
parseGEPrices(BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("item_configs.json"), StandardCharsets.UTF_8)).useLines { it.joinToString("\n") })
} catch (e: Exception) {
emptyMap()
}
private var gePriceMap = loadGEPrices()
private val colorMap = mapOf(
"tagged" to "#AA00FF",
@ -61,22 +60,23 @@ open class plugin : Plugin() {
)
override fun Init() {
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
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 item_configs.json")
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")
}
private fun isTagged(itemId: Int): Boolean {
return taggedItems.contains(itemId)
return kondoExposed_taggedItems.contains(itemId)
}
private fun isHidden(itemId: Int): Boolean {
return ignoredItems.contains(itemId)
return kondoExposed_ignoredItems.contains(itemId)
}
override fun Draw(timeDelta: Long) = renderGroundItemNames()
@ -141,10 +141,10 @@ open class plugin : Plugin() {
val screenY = screenPos[1]
val color = when {
isTagged(itemDef.id) -> colorMap["tagged"]
highestValue < lowValue -> "#FFFFFF"
highestValue < mediumValue -> colorMap["lowValue"]
highestValue < highValue -> colorMap["mediumValue"]
highestValue < insaneValue -> colorMap["highValue"]
highestValue < kondoExposed_lowValue -> "#FFFFFF"
highestValue < kondoExposed_mediumValue -> colorMap["lowValue"]
highestValue < kondoExposed_highValue -> colorMap["mediumValue"]
highestValue < kondoExposed_insaneValue -> colorMap["highValue"]
else -> colorMap["insaneValue"]
} ?: "#FFFFFF"
val colorInt = color.drop(1).toInt(16)
@ -164,6 +164,7 @@ open class plugin : Plugin() {
}
}
}
InterfaceList.fullRedrawAllInterfaces() // Prevent an overdraw bug
}
private fun getDisplayedStackSize(objstacknodeLL: LinkedList): Int{
@ -182,7 +183,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 < hideBelowValue || isHidden(itemDef.id)) && !isTagged(itemDef.id))
return !((highestValue < kondoExposed_hideBelowValue || isHidden(itemDef.id)) && !isTagged(itemDef.id))
}
override fun OnMiniMenuCreate(currentEntries: Array<out MiniMenuEntry>?) {
@ -199,7 +200,7 @@ open class plugin : Plugin() {
private fun ignoreItem(itemId: Int): Runnable {
return Runnable {
val existingIgnores = ignoredItems.toMutableList()
val existingIgnores = kondoExposed_ignoredItems.toMutableList()
if (existingIgnores.contains(itemId)) {
existingIgnores.remove(itemId)
@ -214,7 +215,7 @@ open class plugin : Plugin() {
private fun tagItem(itemId: Int): Runnable {
return Runnable {
val existingTags = taggedItems.toMutableList()
val existingTags = kondoExposed_taggedItems.toMutableList()
if (existingTags.contains(itemId)) {
existingTags.remove(itemId)
@ -229,26 +230,28 @@ open class plugin : Plugin() {
private fun resetConfig() {
lowValue = 5000
mediumValue = 20000
highValue = 50000
insaneValue = 100000
hideBelowValue = 0
kondoExposed_lowValue = 5000
kondoExposed_mediumValue = 20000
kondoExposed_highValue = 50000
kondoExposed_insaneValue = 100000
kondoExposed_hideBelowValue = 0
kondoExposed_useLiveGEPrices = true
StoreData("ground-item-tags","");
StoreData("ground-item-ignore","");
StoreData("low-value", lowValue)
StoreData("medium-value", mediumValue)
StoreData("high-value", highValue)
StoreData("insane-value", insaneValue)
StoreData("hide-below-value", hideBelowValue)
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)
}
private fun displayRanges() {
val low = lowValue
val medium = mediumValue
val high = highValue
val insane = insaneValue
val hide = hideBelowValue
val low = kondoExposed_lowValue
val medium = kondoExposed_mediumValue
val high = kondoExposed_highValue
val insane = kondoExposed_insaneValue
val hide = kondoExposed_hideBelowValue
SendMessage("== Ground Item Config ==")
SendMessage("Low: $low")
@ -257,12 +260,12 @@ open class plugin : Plugin() {
SendMessage("Insane: $insane")
SendMessage("Hide Below: $hide")
SendMessage("-- Ignored Items --")
for(item in ignoredItems){
for(item in kondoExposed_ignoredItems){
val itemDef = ObjTypeList.get(item)
SendMessage("Ignored: ${itemDef.name} ${itemDef.id}")
}
SendMessage("-- Tagged Items --")
for(item in taggedItems){
for(item in kondoExposed_taggedItems){
val itemDef = ObjTypeList.get(item)
SendMessage("Tagged: ${itemDef.name} ${itemDef.id}")
}
@ -274,19 +277,74 @@ open class plugin : Plugin() {
DrawText(FontType.SMALL, fromColor(Color(color)), TextModifier.CENTER, text, x, y)
}
private fun parseGEPrices(json: String): Map<String, String> {
return json
.trim()
.removeSurrounding("[", "]")
.split("},")
.associate { obj ->
val pairs = obj.trim().removeSurrounding("{", "}").split(",")
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)
gePriceMap = loadGEPrices();
}
fun loadGEPrices(): Map<String, String> {
return if (kondoExposed_useLiveGEPrices) {
try {
println("GroundItems: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0")
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val inputStream = connection.inputStream
val content = inputStream.bufferedReader().use(BufferedReader::readText)
val items = content.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",")
val itemId = pairs.find { it.trim().startsWith("\"item_id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val value = pairs.find { it.trim().startsWith("\"value\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (itemId != null && value != null) {
gePrices[itemId] = value
}
}
gePrices
} else {
emptyMap()
}
} catch (e: Exception) {
emptyMap()
}
} else {
try {
println("GroundItems: Loading Local GE Prices")
BufferedReader(InputStreamReader(GroundItems.plugin::class.java.getResourceAsStream("item_configs.json"), StandardCharsets.UTF_8))
.useLines { lines ->
val json = lines.joinToString("\n")
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",")
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val grandExchangePrice =
pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()
?.trim('\"')
id to grandExchangePrice
}.filterKeys { it != null }.filterValues { it != null } as Map<String, String>
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (id != null && grandExchangePrice != null) {
gePrices[id] = grandExchangePrice
}
}
gePrices
}
} catch (e: Exception) {
emptyMap()
}
}
}
private fun formatValue(value: Int): String {

View file

@ -0,0 +1,90 @@
package KondoKit
import java.awt.Color
import java.awt.Dimension
import javax.swing.JPanel
object Helpers {
fun getSpriteId(skillId: Int) : Int {
return when (skillId) {
0 -> 197
1 -> 199
2 -> 198
3 -> 203
4 -> 200
5 -> 201
6 -> 202
7 -> 212
8 -> 214
9 -> 208
10 -> 211
11 -> 213
12 -> 207
13 -> 210
14 -> 209
15 -> 205
16 -> 204
17 -> 206
18 -> 216
19 -> 217
20 -> 215
21 -> 220
22 -> 221
23 -> 222
else -> 222
}
}
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>"
}
fun formatNumber(value: Int): String {
return when {
value >= 1_000_000 -> String.format("%.2fM", value / 1_000_000.0)
value >= 1_000 -> String.format("%.2fK", value / 1_000.0)
else -> value.toString()
}
}
fun getProgressBarColor(skillId: Int): Color {
// Straight from runelite
return when (skillId) {
0 -> Color(155, 32, 7) // ATTACK
1 -> Color(98, 119, 190) // DEFENCE
2 -> Color(4, 149, 90) // STRENGTH
3 -> Color(131, 126, 126) // HITPOINTS
4 -> Color(109, 144, 23) // RANGED
5 -> Color(159, 147, 35) // PRAYER
6 -> Color(50, 80, 193) // MAGIC
7 -> Color(112, 35, 134) // COOKING
8 -> Color(52, 140, 37) // WOODCUTTING
9 -> Color(3, 141, 125) // FLETCHING
10 -> Color(106, 132, 164) // FISHING
11 -> Color(189, 120, 25) // FIREMAKING
12 -> Color(151, 110, 77) // CRAFTING
13 -> Color(108, 107, 82) // SMITHING
14 -> Color(93, 143, 167) // MINING
15 -> Color(7, 133, 9) // HERBLORE
16 -> Color(58, 60, 137) // AGILITY
17 -> Color(108, 52, 87) // THIEVING
18 -> Color(100, 100, 100) // SLAYER
19 -> Color(101, 152, 63) // FARMING
20 -> Color(170, 141, 26) // RUNECRAFT
21 -> Color(92, 89, 65) // HUNTER
22 -> Color(130, 116, 95) // CONSTRUCTION
23 -> Color(150, 50, 50) // Placeholder for any additional skill
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

@ -0,0 +1,462 @@
package KondoKit
import KondoKit.Constants.COLOR_BACKGROUND_DARK
import KondoKit.Helpers.getSpriteId
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import com.google.gson.Gson
import plugin.api.API
import rt4.Sprites
import java.awt.*
import java.awt.datatransfer.DataFlavor
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import javax.swing.*
import javax.swing.border.MatteBorder
object Constants {
// Sprite IDs
const val COMBAT_LVL_SPRITE = 168
const val IRONMAN_SPRITE = 4
const val MAG_SPRITE = 1423
const val LVL_BAR_SPRITE = 898
// Dimensions
val SEARCH_FIELD_DIMENSION = Dimension(270, 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 IMAGE_CANVAS_DIMENSION = Dimension(20, 20)
val NUMBER_LABEL_DIMENSION = Dimension(30, 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)
// Fonts
val FONT_ARIAL_PLAIN_14 = Font("Arial", Font.PLAIN, 14)
val FONT_ARIAL_PLAIN_12 = Font("Arial", Font.PLAIN, 12)
val FONT_ARIAL_BOLD_12 = Font("Arial", Font.BOLD, 12)
}
var text: String = ""
object HiscoresView {
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 {
ImageCanvas(it).apply {
preferredSize = Constants.ICON_DIMENSION_SMALL
size = preferredSize
minimumSize = preferredSize
maximumSize = preferredSize
}
}
init {
preferredSize = Constants.SEARCH_FIELD_DIMENSION
background = Constants.COLOR_BACKGROUND_DARK
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_PLAIN_14
minimumSize = preferredSize
maximumSize = preferredSize
addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
// Prevent null character from being typed on Ctrl+A & Ctrl+V
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
e.consume()
return
}
if (e.keyChar == '\b') {
if (text.isNotEmpty()) {
text = text.dropLast(1)
}
} else if (e.keyChar == '\n') {
searchPlayer(text)
} else {
text += e.keyChar
}
repaint()
}
override fun keyPressed(e: KeyEvent) {
if (e.isControlDown) {
when (e.keyCode) {
KeyEvent.VK_A -> {
text = ""
repaint()
}
KeyEvent.VK_V -> {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
text += pasteText
repaint()
}
}
}
}
})
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (e.x > width - 20 && e.y < 20) {
text = ""
repaint()
}
}
})
Timer(500) {
cursorVisible = !cursorVisible
if(plugin.StateManager.focusedView == "HISCORE_SEARCH_VIEW")
repaint()
}.start()
}
override fun paint(g: Graphics) {
super.paint(g)
g.color = foreground
g.font = font
val fm = g.fontMetrics
val cursorX = fm.stringWidth(text) + 30
imageCanvas?.let { canvas ->
val imgG = g.create(5, 5, canvas.width, canvas.height)
canvas.paint(imgG)
imgG.dispose()
}
g.drawString(text, 30, 20)
if (cursorVisible && hasFocus()) {
g.drawLine(cursorX, 5, cursorX, 25)
}
if (text.isNotEmpty()) {
g.color = Constants.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()}"
Thread {
try {
val url = URL(apiUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.use { it.readText() }
reader.close()
SwingUtilities.invokeLater {
updatePlayerData(response, username)
}
} else {
SwingUtilities.invokeLater {
showError("Player not found!")
}
}
} catch (e: Exception) {
SwingUtilities.invokeLater {
showError("Error fetching data!")
}
}
}.start()
}
private fun updatePlayerData(jsonResponse: String, username: String) {
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
updateHiscoresView(hiscoresResponse, username)
}
private fun updateHiscoresView(data: HiscoresResponse, username: String) {
val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel
val ironMode = data.info.iron_mode
playerNameLabel?.removeAll() // Clear previous components
if (ironMode != "0") {
val ironmanBufferedImage = getBufferedImageFromSprite(Sprites.nameIcons[Constants.IRONMAN_SPRITE + ironMode.toInt() - 1])
val imageCanvas = ironmanBufferedImage.let {
ImageCanvas(it).apply {
preferredSize = Constants.IMAGE_CANVAS_DIMENSION
size = Constants.IMAGE_CANVAS_DIMENSION
}
}
playerNameLabel?.add(imageCanvas)
}
val nameLabel = JLabel(username, JLabel.CENTER).apply {
font = Constants.FONT_ARIAL_BOLD_12
foreground = Constants.COLOR_FOREGROUND_LIGHT
}
playerNameLabel?.add(nameLabel)
playerNameLabel?.revalidate()
playerNameLabel?.repaint()
// Update skill labels
data.skills.forEachIndexed { index, skill ->
val labelName = "skillLabel_$index"
val numberLabel = findComponentByName(hiscoresPanel, labelName) as? JLabel
numberLabel?.text = skill.static
}
updateTotalAndCombatLevel(data.skills, true)
hiscoresPanel.revalidate()
hiscoresPanel.repaint()
}
private fun updateTotalAndCombatLevel(skills: List<Skill>, isMemberWorld: Boolean) {
val totalLevel = skills.sumBy { it.static.toInt() }
val totalLevelLabel = findComponentByName(hiscoresPanel, "totalLevelLabel") as? JLabel
totalLevelLabel?.text = totalLevel.toString()
val attack = skills.find { it.id == "0" }?.static?.toInt() ?: 1
val defence = skills.find { it.id == "1" }?.static?.toInt() ?: 1
val strength = skills.find { it.id == "2" }?.static?.toInt() ?: 1
val hitpoints = skills.find { it.id == "3" }?.static?.toInt() ?: 1
val ranged = skills.find { it.id == "4" }?.static?.toInt() ?: 1
val prayer = skills.find { it.id == "5" }?.static?.toInt() ?: 1
val magic = skills.find { it.id == "6" }?.static?.toInt() ?: 1
val summoning = skills.find { it.id == "23" }?.static?.toInt() ?: 1
val combatLevel = calculateCombatLevel(attack, defence, strength, hitpoints, prayer, ranged, magic, summoning, true)
val combatLevelLabel = findComponentByName(hiscoresPanel, "combatLevelLabel") as? JLabel
combatLevelLabel?.text = combatLevel.toString()
}
private fun calculateCombatLevel(
attack: Int,
defence: Int,
strength: Int,
hitpoints: Int,
prayer: Int,
ranged: Int,
magic: Int,
summoning: Int,
isMemberWorld: Boolean
): Double {
val base = (defence + hitpoints + Math.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 maxCombatType = maxOf(melee, range, mage)
val summoningFactor = if (isMemberWorld) Math.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) {
return component
}
if (component is Container) {
val child = findComponentByName(component, name)
if (child != null) {
return child
}
}
}
return null
}
}
fun createHiscoreSearchView(): JPanel {
val hiscorePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
name = "HISCORE_SEARCH_VIEW"
background = Constants.COLOR_BACKGROUND_MEDIUM
preferredSize = Constants.HISCORE_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
}
val customSearchField = CustomSearchField(hiscorePanel)
val searchFieldWrapper = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
background = Constants.COLOR_BACKGROUND_MEDIUM
preferredSize = Constants.SEARCH_FIELD_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
alignmentX = Component.CENTER_ALIGNMENT
add(customSearchField)
}
val searchPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = Constants.COLOR_BACKGROUND_MEDIUM
add(searchFieldWrapper)
}
hiscorePanel.add(Helpers.Spacer(height = 10))
hiscorePanel.add(searchPanel)
hiscorePanel.add(Helpers.Spacer(height = 10))
// Adding the player name panel in place of the filterPanel
val playerNamePanel = JPanel().apply {
layout = FlowLayout(FlowLayout.CENTER)
background = VIEW_BACKGROUND_COLOR
preferredSize = Constants.FILTER_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
name = "playerNameLabel"
}
hiscorePanel.add(playerNamePanel)
hiscorePanel.add(Helpers.Spacer(height = 10))
val skillsPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
background = Constants.COLOR_BACKGROUND_MEDIUM
preferredSize = Constants.SKILLS_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
}
for (i in 0 until 24) {
val skillPanel = JPanel().apply {
layout = BorderLayout()
background = Constants.COLOR_SKILL_PANEL
preferredSize = Constants.SKILL_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
border = MatteBorder(5, 0, 0, 0, COLOR_BACKGROUND_DARK)
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(i)))
val imageCanvas = bufferedImageSprite.let {
ImageCanvas(it).apply {
preferredSize = Constants.IMAGE_CANVAS_DIMENSION
size = Constants.IMAGE_CANVAS_DIMENSION
}
}
val numberLabel = JLabel("", JLabel.RIGHT).apply {
name = "skillLabel_$i"
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_PLAIN_12
preferredSize = Constants.NUMBER_LABEL_DIMENSION
minimumSize = Constants.NUMBER_LABEL_DIMENSION
}
val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply {
background = Constants.COLOR_BACKGROUND_DARK
add(imageCanvas)
add(numberLabel)
}
skillPanel.add(imageContainer, BorderLayout.CENTER)
skillsPanel.add(skillPanel)
}
hiscorePanel.add(skillsPanel)
val totalCombatPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
background = COLOR_BACKGROUND_DARK
preferredSize = Constants.TOTAL_COMBAT_PANEL_DIMENSION
maximumSize = preferredSize
minimumSize = preferredSize
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE));
val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply {
preferredSize = Constants.ICON_DIMENSION_LARGE
size = Constants.ICON_DIMENSION_LARGE
}
val totalLevelLabel = JLabel("").apply {
name = "totalLevelLabel"
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_BOLD_12
horizontalAlignment = JLabel.LEFT
iconTextGap = 10
}
val totalLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
background = Constants.COLOR_BACKGROUND_DARK
add(totalLevelIcon)
add(totalLevelLabel)
}
val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE))
val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply {
preferredSize = Constants.ICON_DIMENSION_LARGE
size = Constants.ICON_DIMENSION_LARGE
}
val combatLevelLabel = JLabel("").apply {
name = "combatLevelLabel"
foreground = Constants.COLOR_FOREGROUND_LIGHT
font = Constants.FONT_ARIAL_BOLD_12
horizontalAlignment = JLabel.LEFT
iconTextGap = 10
}
val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
background = Constants.COLOR_BACKGROUND_DARK
add(combatLevelIcon)
add(combatLevelLabel)
}
totalCombatPanel.add(totalLevelPanel)
totalCombatPanel.add(combatLevelPanel)
hiscorePanel.add(totalCombatPanel)
return hiscorePanel
}
data class HiscoresResponse(
val info: PlayerInfo,
val skills: List<Skill>
)
data class PlayerInfo(
val exp_multiplier: String,
val iron_mode: String
)
data class Skill(
val id: String,
val dynamic: String,
val experience: String,
val static: String
)
}

View file

@ -0,0 +1,43 @@
package KondoKit
import java.awt.Canvas
import java.awt.Color
import java.awt.Dimension
import java.awt.Graphics
import java.awt.image.BufferedImage
class ImageCanvas(private val image: BufferedImage) : Canvas() {
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.fillRect(0, 0, width, height)
g.drawImage(image, 0, 0, width, height, this)
}
override fun getPreferredSize(): Dimension {
return Dimension(image.width, image.height)
}
}

View file

@ -0,0 +1,100 @@
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

@ -0,0 +1,403 @@
package KondoKit
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.XPTrackerView.wrappedWidget
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 plugin.api.API
import rt4.NpcTypeList
import rt4.ObjStackNode
import rt4.Player
import rt4.SceneGraph
import java.awt.*
import java.awt.image.BufferedImage
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import javax.swing.*
object LootTrackerView {
private const val SNAPSHOT_LIFESPAN = 10
const val BAG_ICON = 900;
val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>()
var gePriceMap = loadGEPrices()
private val lootItemPanels = mutableMapOf<String, MutableMap<Int, Int>>()
private val npcKillCounts = mutableMapOf<String, Int>()
private var totalTrackerWidget: XPWidget? = null
var lastConfirmedKillNpcId = -1;
fun loadGEPrices(): Map<String, String> {
return if (plugin.kondoExposed_useLiveGEPrices) {
try {
println("LootTracker: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0")
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val inputStream = connection.inputStream
val content = inputStream.bufferedReader().use(BufferedReader::readText)
val items = content.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",")
val itemId = pairs.find { it.trim().startsWith("\"item_id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val value = pairs.find { it.trim().startsWith("\"value\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (itemId != null && value != null) {
gePrices[itemId] = value
}
}
gePrices
} else {
emptyMap()
}
} catch (e: Exception) {
emptyMap()
}
} else {
try {
println("LootTracker: Loading Local GE Prices")
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("item_configs.json"), StandardCharsets.UTF_8))
.useLines { lines ->
val json = lines.joinToString("\n")
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",")
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (id != null && grandExchangePrice != null) {
gePrices[id] = grandExchangePrice
}
}
gePrices
}
} catch (e: Exception) {
emptyMap()
}
}
}
fun createLootTrackerView(): JPanel {
return JPanel().apply {
layout = FlowLayout(FlowLayout.CENTER, 0, 5)
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))
revalidate()
repaint()
}
}
private fun updateTotalKills() {
totalTrackerWidget?.let {
it.totalXpGained += 1
it.xpGainedLabel.text = formatHtmlLabelText("Total Count: ", primaryColor, it.totalXpGained.toString(), secondaryColor)
it.xpGainedLabel.repaint()
}
}
private fun updateTotalValue(newVal: Int) {
totalTrackerWidget?.let {
it.previousXp += newVal
it.xpPerHourLabel.text = formatHtmlLabelText("Total Value: ", primaryColor, formatValue(it.previousXp) + " gp", secondaryColor)
it.panel.repaint()
}
}
private fun createTotalLootWidget(): XPWidget {
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(BAG_ICON))
val l1 = createLabel(formatHtmlLabelText("Total Value: ", primaryColor, "0" + " gp", secondaryColor))
val l2 = createLabel(formatHtmlLabelText("Total Count: ", primaryColor, "0", secondaryColor))
return XPWidget(
skillId = -1,
panel = createWidgetPanel(bufferedImageSprite,l2,l1),
xpGainedLabel = l2,
xpLeftLabel = JLabel(),
actionsRemainingLabel = JLabel(),
xpPerHourLabel = l1,
progressBar = ProgressBar(0.0, Color(150, 50, 50)),
totalXpGained = 0,
startTime = System.currentTimeMillis(),
previousXp = 0
)
}
private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): Panel {
val imageCanvas = ImageCanvas(bufferedImageSprite).apply {
size = Dimension(width, height)
background = WIDGET_COLOR
}
val imageContainer = Panel(FlowLayout()).apply {
background = WIDGET_COLOR
add(imageCanvas)
}
return Panel(BorderLayout(5, 5)).apply {
background = WIDGET_COLOR
preferredSize = TOTAL_XP_WIDGET_SIZE
add(imageContainer, BorderLayout.WEST)
add(createTextPanel(l1,l2), BorderLayout.CENTER)
}
}
private fun createTextPanel(l1 : JLabel, l2: JLabel): Panel {
return Panel(GridLayout(2, 1, 5, 5)).apply {
background = WIDGET_COLOR
add(l1)
add(l2)
}
}
private fun createLabel(text: String): JLabel {
return JLabel(text).apply {
font = Font("Arial", Font.PLAIN, 11)
horizontalAlignment = JLabel.LEFT
}
}
private fun addItemToLootPanel(lootTrackerView: JPanel, drop: Item, npcName: String) {
findLootItemsPanel(lootTrackerView, npcName)?.let { lootPanel ->
val itemQuantities = lootItemPanels.getOrPut(npcName) { mutableMapOf() }
val newQuantity = itemQuantities.merge(drop.id, drop.quantity, Int::plus) ?: drop.quantity
lootPanel.components.filterIsInstance<JPanel>().find { it.getClientProperty("itemId") == drop.id }
?.let { updateItemPanelIcon(it, drop.id, newQuantity) }
?: addNewItemToPanel(lootPanel, drop, newQuantity)
// Recalculate lootPanel size based on the number of unique items.
val totalItems = lootItemPanels[npcName]?.size ?: 0
val rowsNeeded = Math.ceil(totalItems / 6.0).toInt()
val lootPanelHeight = rowsNeeded * 36 + (rowsNeeded - 1)
lootPanel.preferredSize = Dimension(270, lootPanelHeight+10)
lootPanel.revalidate()
lootPanel.repaint()
}
}
private fun addNewItemToPanel(lootPanel: JPanel, drop: Item, newQuantity: Int) {
val itemPanel = createItemPanel(drop.id, newQuantity)
lootPanel.add(itemPanel)
}
private fun createItemPanel(itemId: Int, quantity: Int): JPanel {
val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 0, 0))
return FixedSizePanel(Dimension(36, 32)).apply {
preferredSize = Dimension(36, 32)
background = WIDGET_COLOR
minimumSize = preferredSize
maximumSize = preferredSize
add(ImageCanvas(bufferedImageSprite).apply {
preferredSize = Dimension(36, 32)
background = WIDGET_COLOR
minimumSize = preferredSize
maximumSize = preferredSize
}, BorderLayout.CENTER)
putClientProperty("itemId", itemId)
}
}
private fun updateItemPanelIcon(panel: JPanel, itemId: Int, quantity: Int) {
panel.removeAll()
panel.add(createItemPanel(itemId, quantity).components[0], BorderLayout.CENTER)
panel.revalidate()
panel.repaint()
}
private fun updateKillCountLabel(lootTrackerPanel: JPanel, npcName: String) {
lootTrackerPanel.components.filterIsInstance<JPanel>().forEach { childFramePanel ->
(childFramePanel.components.firstOrNull { it is JPanel } as? JPanel)?.components
?.filterIsInstance<JLabel>()?.find { it.name == "killCountLabel_$npcName" }
?.apply {
text = formatHtmlLabelText(npcName, secondaryColor, " x ${npcKillCounts[npcName]}", primaryColor)
revalidate()
repaint()
}
}
}
private fun updateValueLabel(lootTrackerPanel: JPanel, valueOfNewDrops: String, npcName: String) {
lootTrackerPanel.components.filterIsInstance<JPanel>().forEach { childFramePanel ->
(childFramePanel.components.firstOrNull { it is JPanel } as? JPanel)?.components
?.filterIsInstance<JLabel>()?.find { it.name == "valueLabel_$npcName" }
?.apply {
val newValue = (getClientProperty("val") as? Int ?: 0) + valueOfNewDrops.toInt()
text = "${formatValue(newValue)} gp"
putClientProperty("val", newValue)
revalidate()
repaint()
}
}
}
private fun formatValue(value: Int): String {
return when {
value >= 1_000_000 -> "%.1fM".format(value / 1_000_000.0)
value >= 10_000 -> "%.1fK".format(value / 1_000.0)
else -> DecimalFormat("#,###").format(value)
}
}
private fun findLootItemsPanel(container: Container, npcName: String): JPanel? {
return container.components.filterIsInstance<JPanel>().firstOrNull { it.name == "LOOT_ITEMS_$npcName" }
?: container.components.filterIsInstance<Container>().mapNotNull { findLootItemsPanel(it, npcName) }.firstOrNull()
}
fun onPostClientTick(lootTrackerView: JPanel) {
val toRemove = mutableListOf<Int>()
npcDeathSnapshots.entries.forEach { (npcId, snapshot) ->
val postDeathSnapshot = takeGroundSnapshot(Pair(snapshot.location.first, snapshot.location.second))
val newDrops = postDeathSnapshot.subtract(snapshot.items)
if (newDrops.isNotEmpty()) {
val npcName = NpcTypeList.get(npcId).name
lastConfirmedKillNpcId = npcId;
handleNewDrops(npcName.toString(), newDrops, lootTrackerView)
toRemove.add(npcId)
} else if (snapshot.age >= SNAPSHOT_LIFESPAN) {
toRemove.add(npcId)
} else {
snapshot.age++
}
}
toRemove.forEach { npcDeathSnapshots.remove(it) }
}
fun takeGroundSnapshot(location: Pair<Int, Int>): Set<Item> {
return getGroundItemsAt(location.first, location.second).toSet()
}
private fun getGroundItemsAt(x: Int, z: Int): List<Item> {
val objstacknodeLL = SceneGraph.objStacks[Player.plane][x][z] ?: return emptyList()
val items = mutableListOf<Item>()
var itemNode = objstacknodeLL.head() as ObjStackNode?
while (itemNode != null) {
items.add(Item(itemNode.value.type, itemNode.value.amount))
itemNode = objstacknodeLL.next() as ObjStackNode?
}
return items
}
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.revalidate()
lootTrackerView.repaint()
}
npcKillCounts[npcName] = npcKillCounts.getOrDefault(npcName, 0) + 1
updateKillCountLabel(lootTrackerView, npcName)
updateTotalKills()
newDrops.forEach { drop ->
val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity
updateValueLabel(lootTrackerView, geValue.toString(), npcName)
addItemToLootPanel(lootTrackerView, drop, npcName)
updateTotalValue(geValue)
}
}
private fun createLootFrame(npcName: String): JPanel {
val childFramePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR
minimumSize = Dimension(270, 0)
maximumSize = Dimension(270, 700)
}
val labelPanel = JPanel(BorderLayout()).apply {
background = Color(21, 21, 21)
border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
maximumSize = Dimension(270, 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)
horizontalAlignment = JLabel.LEFT
name = "killCountLabel_$npcName"
}
val valueLabel = JLabel("0 gp").apply {
foreground = Color(200, 200, 200)
font = Font("Arial", Font.PLAIN, 12)
horizontalAlignment = JLabel.RIGHT
name = "valueLabel_$npcName"
}
labelPanel.add(countLabel, BorderLayout.WEST)
labelPanel.add(valueLabel, BorderLayout.EAST)
// Panel to hold loot items, using GridLayout to manage rows and columns.
val lootPanel = JPanel().apply {
background = WIDGET_COLOR
border = BorderFactory.createLineBorder(WIDGET_COLOR, 5)
name = "LOOT_ITEMS_$npcName"
layout = GridLayout(0, 6, 1, 1) // 6 columns, rows will be added dynamically
}
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)
return childFramePanel
}
class FixedSizePanel(private val fixedSize: Dimension) : JPanel() {
override fun getPreferredSize(): Dimension {
return fixedSize
}
override fun getMinimumSize(): Dimension {
return fixedSize
}
override fun getMaximumSize(): Dimension {
return fixedSize
}
}
data class GroundSnapshot(val items: Set<Item>, val location: Pair<Int, Int>, var age: Int)
data class Item(val id: Int, val quantity: Int)
}

View file

@ -0,0 +1,53 @@
package KondoKit
import java.awt.Canvas
import java.awt.Color
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
) : Canvas() {
init {
font = Font("Arial", Font.PLAIN, 12)
}
override fun paint(g: Graphics) {
super.paint(g)
// Draw the filled part of the progress bar
g.color = barColor
val width = (progress * this.width / 100).toInt()
g.fillRect(0, 0, width, this.height)
// Draw the unfilled part of the progress bar
g.color = Color(100, 100, 100)
g.fillRect(width, 0, this.width - width, this.height)
// Draw the current level on the far left
g.color = Color(255, 255, 255)
g.drawString("Lvl. $currentLevel", 5, this.height / 2 + 4)
// 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)
// 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)
}
fun updateProgress(newProgress: Double, currentLevel: Int, nextLevel: Int, isVisible : Boolean) {
this.progress = newProgress
this.currentLevel = currentLevel
this.nextLevel = nextLevel
if(isVisible)
repaint()
}
}

View file

@ -0,0 +1,149 @@
package KondoKit
import KondoKit.KondoKitUtils.convertValue
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import plugin.Plugin
import plugin.PluginRepository
import java.awt.*
import java.lang.reflect.Field
import java.util.*
import java.util.Timer
import javax.swing.*
object ReflectiveEditorView {
fun createReflectiveEditorView(): JPanel {
val reflectiveEditorPanel = JPanel(BorderLayout())
reflectiveEditorPanel.background = VIEW_BACKGROUND_COLOR
reflectiveEditorPanel.add(Box.createVerticalStrut(5))
reflectiveEditorPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
return reflectiveEditorPanel
}
fun addPlugins(reflectiveEditorView: JPanel) {
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)
}
} catch (e: Exception) {
e.printStackTrace()
}
reflectiveEditorView.revalidate()
reflectiveEditorView.repaint()
}
private fun addPluginToEditor(reflectiveEditorView: JPanel, plugin: Any) {
reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS)
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)
label.foreground = primaryColor
label.font = Font("Arial", Font.BOLD, 14)
labelPanel.add(label, BorderLayout.CENTER)
reflectiveEditorView.add(labelPanel)
}
for (field in exposedFields) {
field.isAccessible = true
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
)
}
}
fieldPanel.add(applyButton, gbc)
reflectiveEditorView.add(fieldPanel)
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)
}
}
}
}, 0, 1000)
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(10))
}
}
}

View file

@ -0,0 +1,112 @@
package KondoKit
import rt4.GlIndexedSprite
import rt4.GlSprite
import rt4.SoftwareIndexedSprite
import rt4.SoftwareSprite
import java.awt.image.BufferedImage
object SpriteToBufferedImage {
/**
* Converts a SoftwareSprite back into a BufferedImage.
*
* @param sprite The sprite to be converted.
* @return The BufferedImage created from the sprite.
*/
fun convertToBufferedImage(sprite: SoftwareSprite): 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)
}
}
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
}
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
}
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
}
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)
}
}
}

View file

@ -0,0 +1,357 @@
package KondoKit
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.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.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.totalXPWidget
import XPGlobesPlugin.XPTable
import plugin.api.API
import java.awt.*
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
object XPTrackerView {
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4)
val npcHitpointsMap: Map<Int, Int> = try {
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("npc_hitpoints_map.json"), StandardCharsets.UTF_8))
.useLines { lines ->
val json = lines.joinToString("\n")
val pairs = json.trim().removeSurrounding("{", "}").split(",")
val map = mutableMapOf<Int, Int>()
for (pair in pairs) {
val keyValue = pair.split(":")
val id = keyValue[0].trim().trim('\"').toIntOrNull()
val hitpoints = keyValue[1].trim()
if (id != null && hitpoints.isNotEmpty()) {
map[id] = hitpoints.toIntOrNull() ?: 0
}
}
map
}
} catch (e: Exception) {
println("XPTracker Error parsing NPC HP: ${e.message}")
emptyMap()
}
fun updateWidget(xpWidget: XPWidget, xp: Int) {
val (currentLevel, xpGainedSinceLastLevel) = XPTable.getLevelForXp(xp)
var xpGainedSinceLastUpdate = xp - xpWidget.previousXp
xpWidget.totalXpGained += xpGainedSinceLastUpdate
updateTotalXPWidget(xpGainedSinceLastUpdate)
val progress: Double
if (currentLevel >= 99) {
progress = 100.0 // Set progress to 100% if the level is 99 or above
xpWidget.xpLeftLabel.text = "" // Hide XP Left when level is 99
xpWidget.actionsRemainingLabel.text = ""
} else {
val nextLevelXp = XPTable.getXpRequiredForLevel(currentLevel + 1)
val xpLeft = nextLevelXp - xp
progress = xpGainedSinceLastLevel.toDouble() / (nextLevelXp - XPTable.getXpRequiredForLevel(currentLevel)) * 100
val xpLeftstr = formatNumber(xpLeft)
xpWidget.xpLeftLabel.text = formatHtmlLabelText("XP Left: ", primaryColor, xpLeftstr, secondaryColor)
if(COMBAT_SKILLS.contains(xpWidget.skillId)) {
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
val xpPerKill = when (xpWidget.skillId) {
3 -> kondoExposed_playerXPMultiplier * (npcHP ?: 1) // Hitpoints
else -> kondoExposed_playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
}
val remainingKills = xpLeft / xpPerKill
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
}
} else {
if(xpGainedSinceLastUpdate == 0)
xpGainedSinceLastUpdate = 1 // Avoid possible divide by 0
val remainingActions = (xpLeft / xpGainedSinceLastUpdate).coerceAtLeast(1)
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Actions: ", primaryColor, remainingActions.toString(), secondaryColor)
}
}
val formattedXp = formatNumber(xpWidget.totalXpGained)
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
// Update the progress bar with current level, progress, and next level
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, plugin.StateManager.focusedView == "XP_TRACKER_VIEW")
xpWidget.previousXp = xp
if (plugin.StateManager.focusedView == "XP_TRACKER_VIEW")
xpWidget.panel.repaint()
}
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
val totalXPWidget = plugin.StateManager.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()
}
fun createTotalXPWidget(): XPWidget {
val widgetPanel = Panel().apply {
layout = BorderLayout(5, 5)
background = WIDGET_COLOR
preferredSize = TOTAL_XP_WIDGET_SIZE
maximumSize = TOTAL_XP_WIDGET_SIZE
minimumSize = TOTAL_XP_WIDGET_SIZE
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(898))
val imageContainer = Panel(FlowLayout()).apply {
background = WIDGET_COLOR
preferredSize = IMAGE_SIZE
maximumSize = IMAGE_SIZE
minimumSize = IMAGE_SIZE
size = IMAGE_SIZE
}
bufferedImageSprite.let { image ->
val imageCanvas = ImageCanvas(image).apply {
background = WIDGET_COLOR
preferredSize = Dimension(image.width, image.height)
maximumSize = Dimension(image.width, image.height)
minimumSize = Dimension(image.width, image.height)
size = Dimension(image.width, image.height)
}
imageContainer.add(imageCanvas)
imageContainer.size = Dimension(image.width, image.height)
imageContainer.revalidate()
if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW")
imageContainer.repaint()
}
val textPanel = Panel().apply {
layout = GridLayout(2, 1, 5, 5)
background = WIDGET_COLOR
}
val font = Font("Arial", Font.PLAIN, 11)
val xpGainedLabel = JLabel(
formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val xpPerHourLabel = JLabel(
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
textPanel.add(xpGainedLabel)
textPanel.add(xpPerHourLabel)
widgetPanel.add(imageContainer, BorderLayout.WEST)
widgetPanel.add(textPanel, BorderLayout.CENTER)
return XPWidget(
skillId = -1,
panel = 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)),
totalXpGained = 0,
startTime = System.currentTimeMillis(),
previousXp = 0,
actionsRemainingLabel = JLabel(),
)
}
fun createXPTrackerView(): JPanel? {
val widgetViewPanel = JPanel()
widgetViewPanel.layout = BoxLayout(widgetViewPanel, BoxLayout.Y_AXIS)
widgetViewPanel.background = VIEW_BACKGROUND_COLOR
widgetViewPanel.add(Box.createVerticalStrut(5))
totalXPWidget = createTotalXPWidget()
widgetViewPanel.add(wrappedWidget(totalXPWidget!!.panel))
widgetViewPanel.add(Box.createVerticalStrut(5))
return widgetViewPanel
}
fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
val widgetPanel = Panel().apply {
layout = BorderLayout(5, 5)
background = WIDGET_COLOR
preferredSize = WIDGET_SIZE
maximumSize = WIDGET_SIZE
minimumSize = WIDGET_SIZE
}
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId)))
val imageContainer = Panel(FlowLayout()).apply {
background = WIDGET_COLOR
preferredSize = IMAGE_SIZE
maximumSize = IMAGE_SIZE
minimumSize = IMAGE_SIZE
size = IMAGE_SIZE
}
bufferedImageSprite.let { image ->
val imageCanvas = ImageCanvas(image).apply {
background = WIDGET_COLOR
preferredSize = Dimension(image.width, image.height)
maximumSize = Dimension(image.width, image.height)
minimumSize = Dimension(image.width, image.height)
size = Dimension(image.width, image.height) // Explicitly set the size
}
imageContainer.add(imageCanvas)
imageContainer.size = Dimension(image.width, image.height) // Ensure container respects the image size
imageContainer.revalidate()
if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW")
imageContainer.repaint()
}
val textPanel = Panel().apply {
layout = GridLayout(2, 2, 5, 5)
background = WIDGET_COLOR
}
val font = Font("Arial", Font.PLAIN, 11)
val xpGainedLabel = JLabel(
formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val xpLeftLabel = JLabel(
formatHtmlLabelText("XP Left: ", primaryColor, "0K", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val xpPerHourLabel = JLabel(
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val killsLabel = JLabel(
formatHtmlLabelText("Kills: ", primaryColor, "0", secondaryColor)
).apply {
this.horizontalAlignment = JLabel.LEFT
this.font = font
}
val levelPanel = Panel().apply {
layout = BorderLayout(5, 0)
background = Color(43, 43, 43)
}
val progressBarPanel = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
preferredSize = Dimension(160, 22)
}
levelPanel.add(progressBarPanel, BorderLayout.CENTER)
textPanel.add(xpGainedLabel)
textPanel.add(xpLeftLabel)
textPanel.add(xpPerHourLabel)
textPanel.add(killsLabel)
widgetPanel.add(imageContainer, BorderLayout.WEST)
widgetPanel.add(textPanel, BorderLayout.CENTER)
widgetPanel.add(levelPanel, BorderLayout.SOUTH)
widgetPanel.revalidate()
if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW")
widgetPanel.repaint()
return XPWidget(
skillId = skillId,
panel = widgetPanel,
xpGainedLabel = xpGainedLabel,
xpLeftLabel = xpLeftLabel,
xpPerHourLabel = xpPerHourLabel,
progressBar = progressBarPanel,
totalXpGained = 0,
actionsRemainingLabel = killsLabel,
startTime = System.currentTimeMillis(),
previousXp = previousXp
)
}
fun wrappedWidget(component: Component, padding: Int = 7): Panel {
val outerPanelSize = Dimension(
component.preferredSize.width + 2 * padding,
component.preferredSize.height + 2 * padding
)
val outerPanel = Panel(GridBagLayout()).apply {
background = WIDGET_COLOR
preferredSize = outerPanelSize
maximumSize = outerPanelSize
minimumSize = outerPanelSize
}
val innerPanel = Panel(BorderLayout()).apply {
background = WIDGET_COLOR
preferredSize = component.preferredSize
maximumSize = component.preferredSize
minimumSize = component.preferredSize
add(component, BorderLayout.CENTER)
}
val gbc = GridBagConstraints().apply {
anchor = GridBagConstraints.CENTER
}
outerPanel.add(innerPanel, gbc)
return outerPanel
}
}
data class XPWidget(
val 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
)

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,381 @@
package KondoKit
import KondoKit.Constants.COMBAT_LVL_SPRITE
import KondoKit.Helpers.formatHtmlLabelText
import KondoKit.Helpers.formatNumber
import KondoKit.Helpers.getSpriteId
import KondoKit.HiscoresView.createHiscoreSearchView
import KondoKit.LootTrackerView.BAG_ICON
import KondoKit.LootTrackerView.createLootTrackerView
import KondoKit.LootTrackerView.npcDeathSnapshots
import KondoKit.LootTrackerView.onPostClientTick
import KondoKit.LootTrackerView.takeGroundSnapshot
import KondoKit.ReflectiveEditorView.addPlugins
import KondoKit.ReflectiveEditorView.createReflectiveEditorView
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
import KondoKit.XPTrackerView.createTotalXPWidget
import KondoKit.XPTrackerView.createXPTrackerView
import KondoKit.XPTrackerView.createXPWidget
import KondoKit.XPTrackerView.updateWidget
import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.plugin.StateManager.initialXP
import KondoKit.plugin.StateManager.totalXPWidget
import KondoKit.plugin.StateManager.xpWidgets
import plugin.Plugin
import plugin.annotations.PluginMeta
import plugin.api.*
import plugin.api.API.*
import plugin.api.FontColor.fromColor
import rt4.GameShell
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.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
@PluginMeta(
author = "downthecrop",
description = "A plugin that adds a right-side panel with custom widgets and navigation.",
version = 1.0
)
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
private const val WRENCH_ICON = 907
private const val LVL_ICON = 898
private const val LOOT_ICON = 777
private const val MAG_SPRITE = 1423
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 var accumulatedTime = 0L
private const val tickInterval = 600L
private var pluginsReloaded = false
private var loginScreen = 160;
private var lastLogin = ""
private var initialized = false;
}
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 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()
}
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(" ","_")))
}
}
}
}
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)
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()
frame.revalidate()
frame.repaint()
pluginsReloaded = true
return true
}
override fun OnXPUpdate(skillId: Int, xp: Int) {
if (!initialXP.containsKey(skillId)) {
initialXP[skillId] = xp
return
}
var xpWidget = xpWidgets[skillId]
if (xpWidget != null) {
updateWidget(xpWidget, xp)
} else {
val previousXp = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
xpWidget = createXPWidget(skillId, previousXp)
xpWidgets[skillId] = xpWidget
xpTrackerView?.add(wrappedWidget(xpWidget.panel))
xpTrackerView?.add(Box.createVerticalStrut(5))
xpTrackerView?.revalidate()
if(StateManager.focusedView == "XP_TRACKER_VIEW")
xpTrackerView?.repaint()
updateWidget(xpWidget, xp)
}
}
override fun Draw(timeDelta: Long) {
if (GlRenderer.enabled && GlRenderer.canvasWidth != GameShell.canvasWidth) {
GlRenderer.canvasWidth = GameShell.canvasWidth
GlRenderer.setViewportBounds(0, 0, GameShell.canvasWidth, GameShell.canvasHeight)
}
if (pluginsReloaded) {
InterfaceList.method3712(true) // Gets the resize working correctly
reflectiveEditorView?.let { addPlugins(it) }
pluginsReloaded = false
}
accumulatedTime += timeDelta
if (accumulatedTime >= tickInterval) {
lootTrackerView?.let { onPostClientTick(it) }
accumulatedTime = 0L
}
// 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 Update() {
xpWidgets.values.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()
}
totalXPWidget?.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()
}
}
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 createNavButton(spriteId: Int, viewName: String): JButton {
val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId))
val buttonSize = Dimension(42, 42)
val imageSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
val actionListener = ActionListener {
cardLayout.show(mainContentPanel, viewName)
StateManager.focusedView = viewName
}
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 {
layout = GridBagLayout()
preferredSize = buttonSize
maximumSize = buttonSize
minimumSize = buttonSize
background = WIDGET_COLOR
isFocusPainted = false
isBorderPainted = false
val gbc = GridBagConstraints().apply {
anchor = GridBagConstraints.CENTER
}
add(imageCanvas, gbc)
addActionListener(actionListener)
}
return button
}
object StateManager {
val initialXP: MutableMap<Int, Int> = HashMap()
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
var totalXPWidget: XPWidget? = null
var focusedView: String = ""
}
}