rt4-client/plugin-playground/src/main/kotlin/KondoKit/plugin.kt
2024-10-20 22:21:52 -07:00

521 lines
20 KiB
Kotlin

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.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.createXPTrackerView
import KondoKit.XPTrackerView.createXPWidget
import KondoKit.XPTrackerView.initialXP
import KondoKit.XPTrackerView.resetXPTracker
import KondoKit.XPTrackerView.totalXPWidget
import KondoKit.XPTrackerView.updateWidget
import KondoKit.XPTrackerView.wrappedWidget
import KondoKit.XPTrackerView.xpTrackerView
import KondoKit.XPTrackerView.xpWidgets
import KondoKit.plugin.StateManager.focusedView
import plugin.Plugin
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.*
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Exposed(val description: String = "")
class plugin : Plugin() {
companion object {
val WIDGET_SIZE = Dimension(220, 50)
val TOTAL_XP_WIDGET_SIZE = Dimension(220, 30)
val IMAGE_SIZE = Dimension(20, 20)
val WIDGET_COLOR = Color(30, 30, 30)
val VIEW_BACKGROUND_COLOR = Color(40, 40, 40)
val primaryColor = Color(165, 165, 165) // Color for "XP Gained:"
val secondaryColor = Color(255, 255, 255) // Color for "0"
@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
private const val FIXED_WIDTH = 765
private const val NAVBAR_WIDTH = 30
private const val MAIN_CONTENT_WIDTH = 242
private const val WRENCH_ICON = 907
private const val LOOT_ICON = 777
private const val MAG_SPRITE = 1423
const val LVL_ICON = 898
private lateinit var cardLayout: CardLayout
private lateinit var mainContentPanel: JPanel
private var rightPanelWrapper: JScrollPane? = null
private var accumulatedTime = 0L
private var reloadInterfaces = false
private const val tickInterval = 600L
private var pluginsReloaded = false
private var loginScreen = 160
private var lastLogin = ""
private var initialized = false;
private var lastClickTime = 0L
private var lastUIOffset = 0
}
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?.let { resetXPTracker(it) }
}
lastLogin = Player.usernameInput.toString()
}
override fun Init() {
// Disable Font AA
System.setProperty("sun.java2d.opengl", "false");
System.setProperty("awt.useSystemAAFontSettings", "off");
System.setProperty("swing.aatext", "false");
}
private fun UpdateDisplaySettings() {
val mode = GetWindowMode()
val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH
lastUIOffset = uiOffset
when (mode) {
WindowMode.FIXED -> {
if (frame.width < FIXED_WIDTH + currentScrollPaneWidth + uiOffset) {
frame.setSize(FIXED_WIDTH + currentScrollPaneWidth + uiOffset, frame.height)
}
val difference = frame.width - (FIXED_WIDTH + uiOffset + currentScrollPaneWidth)
GameShell.leftMargin = difference / 2
}
WindowMode.RESIZABLE -> {
GameShell.canvasWidth = frame.width - (currentScrollPaneWidth + uiOffset)
}
}
rightPanelWrapper?.preferredSize = Dimension(currentScrollPaneWidth, frame.height)
rightPanelWrapper?.revalidate()
rightPanelWrapper?.repaint()
}
fun OnKondoValueUpdated(){
StoreData("kondoUseRemoteGE", useLiveGEPrices)
StoreData("kondoPlayerXPMultiplier", playerXPMultiplier)
LootTrackerView.gePriceMap = LootTrackerView.loadGEPrices()
StoreData("kondoLaunchMinimized", launchMinimized)
StoreData("kondoUIOffset", uiOffset)
if(lastUIOffset != uiOffset){
UpdateDisplaySettings()
reloadInterfaces = true
}
}
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
// 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 {
setActiveView("HISCORE_SEARCH_VIEW")
val customSearchField = hiScoreView?.let { HiscoresView.CustomSearchField(it) }
customSearchField?.searchPlayer(username) ?: run {
println("searchView is null or CustomSearchField creation failed.")
}
}
}
override fun OnPluginsReloaded(): Boolean {
if (!initialized) return true
UpdateDisplaySettings()
frame.remove(rightPanelWrapper)
frame.layout = BorderLayout()
frame.add(rightPanelWrapper, 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
}
var xpWidget = xpWidgets[skillId]
if (xpWidget != null) {
updateWidget(xpWidget, xp)
} else {
val previousXp = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
xpWidget = createXPWidget(skillId, previousXp)
xpWidgets[skillId] = xpWidget
xpTrackerView?.add(wrappedWidget(xpWidget.container))
xpTrackerView?.add(Box.createVerticalStrut(5))
xpTrackerView?.revalidate()
if(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) {
reflectiveEditorView?.let { addPlugins(it) }
pluginsReloaded = false
}
if (reloadInterfaces){
InterfaceList.method3712(true) // Gets the resize working correctly
reloadInterfaces = 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) {
loadFont()
try {
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel")
// Modify the UI properties for a dark theme
UIManager.put("control", Color(50, 50, 50)) // Default background for most controls
UIManager.put("info", Color(50, 50, 50))
UIManager.put("nimbusBase", Color(35, 35, 35)) // Base color for Nimbus L&F
UIManager.put("nimbusAlertYellow", Color(255, 220, 35))
UIManager.put("nimbusDisabledText", Color(100, 100, 100))
UIManager.put("nimbusFocus", Color(115, 164, 209))
UIManager.put("nimbusGreen", Color(176, 179, 50))
UIManager.put("nimbusInfoBlue", Color(66, 139, 221))
UIManager.put("nimbusLightBackground", Color(35, 35, 35)) // Background of text fields, etc.
UIManager.put("nimbusOrange", Color(191, 98, 4))
UIManager.put("nimbusRed", Color(169, 46, 34))
UIManager.put("nimbusSelectedText", Color(255, 255, 255))
UIManager.put("nimbusSelectionBackground", Color(75, 110, 175)) // Selection background
UIManager.put("text", Color(230, 230, 230)) // General text color
// Update component tree UI to apply the new theme
SwingUtilities.updateComponentTreeUI(GameShell.frame)
} catch (e : Exception) {
e.printStackTrace()
}
// Restore saved values
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
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!!), "XP_TRACKER_VIEW")
mainContentPanel.add(ScrollablePanel(hiScoreView!!), "HISCORE_SEARCH_VIEW")
mainContentPanel.add(ScrollablePanel(lootTrackerView!!), "LOOT_TRACKER_VIEW")
mainContentPanel.add(ScrollablePanel(reflectiveEditorView!!), "REFLECTIVE_EDITOR_VIEW")
val navPanel = Panel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
background = WIDGET_COLOR
preferredSize = Dimension(NAVBAR_WIDTH, 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"))
var 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() // Removes the border completely
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER
}
frame.layout = BorderLayout()
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
if(!launchMinimized){
setActiveView("XP_TRACKER_VIEW")
} else {
setActiveView("HIDDEN")
}
initialized = true
pluginsReloaded = true
}
}
override fun Update() {
val widgets = xpWidgets.values
val totalXP = totalXPWidget
widgets.forEach { xpWidget ->
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedXpPerHour = formatNumber(xpPerHour)
xpWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
xpWidget.container.repaint()
}
totalXP?.let { totalXPWidget ->
val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0
val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
totalXPWidget.xpPerHourLabel.text =
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
totalXPWidget.container.repaint()
}
}
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 setActiveView(viewName: String) {
// Handle the visibility of the main content panel
if (viewName == "HIDDEN") {
mainContentPanel.isVisible = false
} else {
if (!mainContentPanel.isVisible) {
mainContentPanel.isVisible = true
}
cardLayout.show(mainContentPanel, viewName)
}
reloadInterfaces = true
UpdateDisplaySettings()
// Revalidate and repaint necessary panels
mainContentPanel.revalidate()
mainContentPanel.repaint()
rightPanelWrapper?.revalidate()
rightPanelWrapper?.repaint()
frame?.revalidate()
frame?.repaint()
focusedView = viewName
}
private fun createNavButton(spriteId: Int, viewName: String): JPanel {
val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId))
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
}
// 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
isOpaque = true // Ensure background is painted
val gbc = GridBagConstraints().apply {
anchor = GridBagConstraints.CENTER
fill = GridBagConstraints.NONE // Prevents stretching
}
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 panelButton
}
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 {
var focusedView: String = ""
}
}