Files
Runelite-EssentialsPlus/src/main/java/com/example/livedashboard/LiveDashboardPlugin.java
T

710 lines
18 KiB
Java
Raw Normal View History

2026-09-18 17:18:53 +02:00
package com.example.livedashboard;
import com.example.livedashboard.overlay.GpsPathOverlay;
import com.example.livedashboard.pathfinding.Pathfinder;
import com.example.livedashboard.pathfinding.ShortcutData;
import com.example.livedashboard.ui.LiveDashboardPanel;
import com.google.inject.Provides;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.EquipmentInventorySlot;
import net.runelite.api.GameState;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemComposition;
import net.runelite.api.ItemContainer;
import net.runelite.api.Player;
import net.runelite.api.Skill;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.ItemContainerChanged;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.game.ItemManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.ClientToolbar;
import net.runelite.client.ui.NavigationButton;
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.util.ImageUtil;
@Slf4j
@PluginDescriptor(
name = "Live Web Dashboard",
description = "Zeigt Echtzeit-Koordinaten, Vitals, Map, Hindernis-GPS-Navigation, Ausruestung, Inventar und Bank an",
tags = {"telemetry", "web", "dashboard", "live", "coordinates", "coords", "map", "bank", "inventory", "equipment", "ge", "gps"}
)
public class LiveDashboardPlugin extends Plugin
{
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private ClientToolbar clientToolbar;
@Inject
private OverlayManager overlayManager;
@Inject
private GpsPathOverlay gpsPathOverlay;
@Inject
private ItemManager itemManager;
@Inject
private LiveDashboardConfig config;
private LiveDashboardServer server;
private LiveDashboardPanel panel;
private NavigationButton navButton;
private int tickCounter = 0;
@Getter
private volatile WorldPoint gpsTarget = null;
@Getter
private volatile List<WorldPoint> gpsPath = Collections.emptyList();
@Getter
private volatile List<ShortcutData> nearbyShortcuts = Collections.emptyList();
private WorldPoint lastCalculatedPlayerPos = null;
private WorldPoint lastCalculatedTargetPos = null;
// Cached containers (Bank persists once opened during session)
private List<Map<String, Object>> cachedInventory = new ArrayList<>();
private List<Map<String, Object>> cachedEquipment = new ArrayList<>();
private List<Map<String, Object>> cachedBank = new ArrayList<>();
// Total item count index (Item ID -> total qty across inv, equip, bank)
private Map<Integer, Map<String, Integer>> cachedItemSummary = new HashMap<>();
// Natural regeneration trackers
private int hpRegenTicksLeft = 100;
private int lastHp = -1;
private int energyRegenTicksLeft = 10;
private int lastEnergy = -1;
private WorldPoint lastWorldPoint = null;
@Provides
LiveDashboardConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(LiveDashboardConfig.class);
}
@Override
protected void startUp() throws Exception
{
log.info("Live Dashboard Plugin startet...");
server = new LiveDashboardServer(this);
panel = new LiveDashboardPanel(this);
BufferedImage icon = null;
try
{
icon = ImageUtil.loadImageResource(getClass(), "/com/example/livedashboard/icon.png");
}
catch (Exception e)
{
try
{
icon = ImageUtil.loadImageResource(getClass(), "/com/example/icon.png");
}
catch (Exception ignored)
{
}
}
navButton = NavigationButton.builder()
.tooltip("Live Web Dashboard")
.icon(icon)
.priority(6)
.panel(panel)
.build();
clientToolbar.addNavigation(navButton);
overlayManager.add(gpsPathOverlay);
startServer(config.port());
clientThread.invokeLater(this::collectAndSendTelemetry);
}
@Override
protected void shutDown() throws Exception
{
log.info("Live Dashboard Plugin stoppt...");
overlayManager.remove(gpsPathOverlay);
if (server != null)
{
server.stop();
server = null;
}
if (navButton != null)
{
clientToolbar.removeNavigation(navButton);
navButton = null;
}
panel = null;
setGpsTarget(null);
cachedInventory.clear();
cachedEquipment.clear();
cachedBank.clear();
cachedItemSummary.clear();
}
public void setGpsTarget(WorldPoint target)
{
this.gpsTarget = target;
if (target == null)
{
this.gpsPath = Collections.emptyList();
this.nearbyShortcuts = Collections.emptyList();
this.lastCalculatedPlayerPos = null;
this.lastCalculatedTargetPos = null;
}
else
{
clientThread.invokeLater(this::recalculatePath);
}
}
public void toggleServer()
{
if (server != null && server.isRunning())
{
server.stop();
if (panel != null)
{
panel.updateServerState(false, -1);
}
}
else
{
startServer(config.port());
}
}
private void startServer(int port)
{
try
{
if (server == null)
{
server = new LiveDashboardServer(this);
}
server.start(port);
if (panel != null)
{
panel.updateServerState(true, server.getPort());
}
}
catch (IOException e)
{
log.error("Fehler beim Starten des Dashboard Webservers auf Port {}", port, e);
if (panel != null)
{
panel.updateServerState(false, -1);
}
}
}
public String getServerUrl()
{
return server != null ? server.getUrl() : "";
}
@Subscribe
public void onGameTick(GameTick event)
{
tickCounter++;
updateRegenTickers();
recalculatePath();
collectAndSendTelemetry();
}
private void recalculatePath()
{
if (gpsTarget == null || client.getGameState() != GameState.LOGGED_IN || client.getLocalPlayer() == null)
{
return;
}
WorldPoint playerPos = client.getLocalPlayer().getWorldLocation();
if (playerPos == null)
{
return;
}
if (playerPos.equals(gpsTarget))
{
setGpsTarget(null);
return;
}
// Dynamically recompute whenever the player moves or target changes
if (!playerPos.equals(lastCalculatedPlayerPos) || !gpsTarget.equals(lastCalculatedTargetPos))
{
lastCalculatedPlayerPos = playerPos;
lastCalculatedTargetPos = gpsTarget;
gpsPath = Pathfinder.findPath(client, playerPos, gpsTarget);
nearbyShortcuts = Pathfinder.findNearbyShortcuts(client, gpsPath);
}
}
private void updateRegenTickers()
{
if (client.getGameState() != GameState.LOGGED_IN)
{
return;
}
int currentHp = client.getBoostedSkillLevel(Skill.HITPOINTS);
boolean rapidHeal = client.isPrayerActive(net.runelite.api.Prayer.RAPID_HEAL);
int hpMaxPeriod = rapidHeal ? 50 : 100;
if (lastHp != -1 && currentHp != lastHp)
{
hpRegenTicksLeft = hpMaxPeriod;
}
else
{
hpRegenTicksLeft--;
if (hpRegenTicksLeft <= 0)
{
hpRegenTicksLeft = hpMaxPeriod;
}
}
lastHp = currentHp;
int currentEnergy = client.getEnergy() / 100;
if (currentEnergy >= 100)
{
energyRegenTicksLeft = 0;
}
else
{
if (lastEnergy != -1 && currentEnergy > lastEnergy)
{
int agility = client.getRealSkillLevel(Skill.AGILITY);
energyRegenTicksLeft = Math.max(2, (int) Math.round((100.0 - (agility * 0.6)) / 10.0));
}
else
{
if (energyRegenTicksLeft > 0)
{
energyRegenTicksLeft--;
}
else
{
int agility = client.getRealSkillLevel(Skill.AGILITY);
energyRegenTicksLeft = Math.max(2, (int) Math.round((100.0 - (agility * 0.6)) / 10.0));
}
}
}
lastEnergy = currentEnergy;
}
@Subscribe
public void onItemContainerChanged(ItemContainerChanged event)
{
int containerId = event.getContainerId();
if (containerId == InventoryID.INVENTORY.getId())
{
cachedInventory = serializeContainer(event.getItemContainer(), false);
rebuildItemSummary();
}
else if (containerId == InventoryID.EQUIPMENT.getId())
{
cachedEquipment = serializeEquipmentContainer(event.getItemContainer());
rebuildItemSummary();
}
else if (containerId == InventoryID.BANK.getId())
{
cachedBank = serializeContainer(event.getItemContainer(), true);
rebuildItemSummary();
}
}
private void rebuildItemSummary()
{
Map<Integer, Map<String, Integer>> map = new HashMap<>();
for (Map<String, Object> item : cachedInventory)
{
if (item == null) continue;
Object idObj = item.get("id");
Object qtyObj = item.get("qty");
if (idObj == null) continue;
int id = (int) idObj;
int qty = qtyObj instanceof Number ? ((Number) qtyObj).intValue() : 1;
Map<String, Integer> counts = map.computeIfAbsent(id, k -> new HashMap<>());
counts.put("inv", counts.getOrDefault("inv", 0) + qty);
}
for (Map<String, Object> item : cachedEquipment)
{
if (item == null) continue;
Object idObj = item.get("id");
Object qtyObj = item.get("qty");
if (idObj == null) continue;
int id = (int) idObj;
int qty = qtyObj instanceof Number ? ((Number) qtyObj).intValue() : 1;
Map<String, Integer> counts = map.computeIfAbsent(id, k -> new HashMap<>());
counts.put("equip", counts.getOrDefault("equip", 0) + qty);
}
for (Map<String, Object> item : cachedBank)
{
if (item == null) continue;
Object idObj = item.get("id");
Object qtyObj = item.get("qty");
if (idObj == null) continue;
int id = (int) idObj;
int qty = qtyObj instanceof Number ? ((Number) qtyObj).intValue() : 1;
Map<String, Integer> counts = map.computeIfAbsent(id, k -> new HashMap<>());
counts.put("bank", counts.getOrDefault("bank", 0) + qty);
}
cachedItemSummary = map;
}
private List<Map<String, Object>> serializeContainer(ItemContainer container, boolean isBank)
{
List<Map<String, Object>> list = new ArrayList<>();
if (container == null)
{
return list;
}
Item[] items = container.getItems();
for (int i = 0; i < items.length; i++)
{
Item item = items[i];
if (item == null || item.getId() <= 0)
{
if (!isBank)
{
Map<String, Object> emptyMap = new LinkedHashMap<>();
emptyMap.put("slot", i);
emptyMap.put("id", 0);
emptyMap.put("qty", 0);
list.add(emptyMap);
}
continue;
}
Map<String, Object> itemMap = new LinkedHashMap<>();
itemMap.put("slot", i);
itemMap.put("id", item.getId());
itemMap.put("qty", item.getQuantity());
ItemComposition comp = itemManager.getItemComposition(item.getId());
if (comp != null)
{
itemMap.put("name", comp.getName());
itemMap.put("members", comp.isMembers());
itemMap.put("haPrice", comp.getHaPrice());
}
else
{
itemMap.put("name", "Item #" + item.getId());
}
int gePrice = itemManager.getItemPrice(item.getId());
itemMap.put("price", gePrice);
list.add(itemMap);
}
return list;
}
private List<Map<String, Object>> serializeEquipmentContainer(ItemContainer container)
{
List<Map<String, Object>> list = new ArrayList<>();
if (container == null)
{
return list;
}
EquipmentInventorySlot[] slots = {
EquipmentInventorySlot.HEAD,
EquipmentInventorySlot.CAPE,
EquipmentInventorySlot.AMULET,
EquipmentInventorySlot.AMMO,
EquipmentInventorySlot.WEAPON,
EquipmentInventorySlot.BODY,
EquipmentInventorySlot.SHIELD,
EquipmentInventorySlot.LEGS,
EquipmentInventorySlot.GLOVES,
EquipmentInventorySlot.BOOTS,
EquipmentInventorySlot.RING
};
for (EquipmentInventorySlot slot : slots)
{
int slotIdx = slot.getSlotIdx();
Item item = container.getItem(slotIdx);
Map<String, Object> slotMap = new LinkedHashMap<>();
slotMap.put("slotName", slot.name());
slotMap.put("slotIdx", slotIdx);
if (item != null && item.getId() > 0)
{
slotMap.put("id", item.getId());
slotMap.put("qty", item.getQuantity());
ItemComposition comp = itemManager.getItemComposition(item.getId());
if (comp != null)
{
slotMap.put("name", comp.getName());
slotMap.put("members", comp.isMembers());
slotMap.put("haPrice", comp.getHaPrice());
}
else
{
slotMap.put("name", "Item #" + item.getId());
}
slotMap.put("price", itemManager.getItemPrice(item.getId()));
}
else
{
slotMap.put("id", 0);
}
list.add(slotMap);
}
return list;
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
if (event.getGameState() == GameState.LOGIN_SCREEN)
{
cachedInventory.clear();
cachedEquipment.clear();
cachedBank.clear();
cachedItemSummary.clear();
setGpsTarget(null);
}
collectAndSendTelemetry();
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (!LiveDashboardConfig.GROUP.equals(event.getGroup()))
{
return;
}
if ("port".equals(event.getKey()))
{
if (server != null && server.isRunning())
{
startServer(config.port());
}
}
}
private void collectAndSendTelemetry()
{
Map<String, Object> data = new LinkedHashMap<>();
GameState gs = client.getGameState();
data.put("gameState", gs != null ? gs.name() : "UNKNOWN");
data.put("gameTick", tickCounter);
if (gs == GameState.LOGGED_IN)
{
Player player = client.getLocalPlayer();
boolean isMoving = false;
if (player != null)
{
data.put("player", player.getName());
WorldPoint wp = player.getWorldLocation();
if (wp != null)
{
data.put("worldX", wp.getX());
data.put("worldY", wp.getY());
data.put("plane", wp.getPlane());
data.put("regionId", wp.getRegionID());
data.put("regionX", wp.getRegionX());
data.put("regionY", wp.getRegionY());
if (lastWorldPoint != null && !lastWorldPoint.equals(wp))
{
isMoving = true;
}
lastWorldPoint = wp;
}
LocalPoint lp = player.getLocalLocation();
if (lp != null)
{
data.put("sceneX", lp.getSceneX());
data.put("sceneY", lp.getSceneY());
}
data.put("orientation", player.getOrientation());
data.put("animation", player.getAnimation());
data.put("poseAnimation", player.getPoseAnimation());
if (player.getPoseAnimation() != player.getIdlePoseAnimation())
{
isMoving = true;
}
}
data.put("isMoving", isMoving);
data.put("world", client.getWorld());
data.put("cameraYaw", client.getCameraYaw());
data.put("cameraPitch", client.getCameraPitch());
data.put("weight", client.getWeight());
// Vitals
int hp = client.getBoostedSkillLevel(Skill.HITPOINTS);
int maxHp = client.getRealSkillLevel(Skill.HITPOINTS);
data.put("hp", hp);
data.put("maxHp", maxHp);
data.put("hpRegenSeconds", (int) Math.ceil(hpRegenTicksLeft * 0.6));
data.put("hpRegenTicks", hpRegenTicksLeft);
data.put("prayer", client.getBoostedSkillLevel(Skill.PRAYER));
data.put("maxPrayer", client.getRealSkillLevel(Skill.PRAYER));
int energy = client.getEnergy() / 100;
data.put("energy", energy);
data.put("energyRegenSeconds", energy >= 100 ? 0 : (int) Math.ceil(energyRegenTicksLeft * 0.6));
data.put("energyRegenTicks", energy >= 100 ? 0 : energyRegenTicksLeft);
int agility = client.getRealSkillLevel(Skill.AGILITY);
data.put("agilityLevel", agility);
// Containers
if (cachedInventory.isEmpty())
{
ItemContainer inv = client.getItemContainer(InventoryID.INVENTORY);
if (inv != null)
{
cachedInventory = serializeContainer(inv, false);
}
}
if (cachedEquipment.isEmpty())
{
ItemContainer equip = client.getItemContainer(InventoryID.EQUIPMENT);
if (equip != null)
{
cachedEquipment = serializeEquipmentContainer(equip);
}
}
if (cachedBank.isEmpty())
{
ItemContainer bank = client.getItemContainer(InventoryID.BANK);
if (bank != null)
{
cachedBank = serializeContainer(bank, true);
}
}
if (cachedItemSummary.isEmpty() && (!cachedInventory.isEmpty() || !cachedEquipment.isEmpty() || !cachedBank.isEmpty()))
{
rebuildItemSummary();
}
long invCount = cachedInventory.stream().filter(m -> ((Number) m.getOrDefault("id", 0)).intValue() > 0).count();
data.put("inventory", cachedInventory);
data.put("invCount", invCount);
long equipCount = cachedEquipment.stream().filter(m -> ((Number) m.getOrDefault("id", 0)).intValue() > 0).count();
data.put("equipment", cachedEquipment);
data.put("equipCount", equipCount);
data.put("bank", cachedBank);
data.put("bankCount", cachedBank.size());
data.put("itemSummary", cachedItemSummary);
// Active GPS target & obstacle-free path & ETA estimation
if (gpsTarget != null)
{
Map<String, Object> gpsMap = new LinkedHashMap<>();
gpsMap.put("x", gpsTarget.getX());
gpsMap.put("y", gpsTarget.getY());
gpsMap.put("plane", gpsTarget.getPlane());
List<Map<String, Integer>> pathList = new ArrayList<>();
if (gpsPath != null)
{
for (WorldPoint p : gpsPath)
{
Map<String, Integer> pMap = new LinkedHashMap<>();
pMap.put("x", p.getX());
pMap.put("y", p.getY());
pathList.add(pMap);
}
}
gpsMap.put("path", pathList);
int totalTiles = (gpsPath != null && !gpsPath.isEmpty())
? gpsPath.size()
: (player != null && player.getWorldLocation() != null ? player.getWorldLocation().distanceTo(gpsTarget) : 0);
gpsMap.put("totalTiles", totalTiles);
// Running = 2 tiles/tick (1.2s per 2 tiles = 0.6s per tile average)
// Walking = 1 tile/tick (0.6s per tile)
int runSeconds = (int) Math.ceil((totalTiles / 2.0) * 0.6);
int walkSeconds = (int) Math.ceil(totalTiles * 0.6);
gpsMap.put("runEtaSeconds", runSeconds);
gpsMap.put("walkEtaSeconds", walkSeconds);
// Nearby Agility Shortcuts along route
List<Map<String, Object>> scList = new ArrayList<>();
if (nearbyShortcuts != null)
{
for (ShortcutData sc : nearbyShortcuts)
{
Map<String, Object> m = new LinkedHashMap<>();
m.put("name", sc.getName());
m.put("level", sc.getRequiredLevel());
m.put("desc", sc.getDescription());
m.put("canUse", agility >= sc.getRequiredLevel());
scList.add(m);
}
}
gpsMap.put("shortcuts", scList);
data.put("gpsTarget", gpsMap);
}
}
if (server != null)
{
server.updateTelemetry(data);
}
if (panel != null)
{
panel.updateTelemetry(data);
}
}
}