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 = "Shows real-time coordinates, stats, map, GPS navigation around obstacles, equipment, inventory, and bank contents", 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 gpsPath = Collections.emptyList(); @Getter private volatile List nearbyShortcuts = Collections.emptyList(); private WorldPoint lastCalculatedPlayerPos = null; private WorldPoint lastCalculatedTargetPos = null; // Cached containers (Bank persists once opened during session) private List> cachedInventory = new ArrayList<>(); private List> cachedEquipment = new ArrayList<>(); private List> cachedBank = new ArrayList<>(); // Total item count index (Item ID -> total qty across inv, equip, bank) private Map> 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> map = new HashMap<>(); for (Map 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 counts = map.computeIfAbsent(id, k -> new HashMap<>()); counts.put("inv", counts.getOrDefault("inv", 0) + qty); } for (Map 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 counts = map.computeIfAbsent(id, k -> new HashMap<>()); counts.put("equip", counts.getOrDefault("equip", 0) + qty); } for (Map 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 counts = map.computeIfAbsent(id, k -> new HashMap<>()); counts.put("bank", counts.getOrDefault("bank", 0) + qty); } cachedItemSummary = map; } private List> serializeContainer(ItemContainer container, boolean isBank) { List> 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 emptyMap = new LinkedHashMap<>(); emptyMap.put("slot", i); emptyMap.put("id", 0); emptyMap.put("qty", 0); list.add(emptyMap); } continue; } Map 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()); } long gePrice = itemManager.getItemPrice(item.getId()); itemMap.put("price", gePrice); list.add(itemMap); } return list; } private List> serializeEquipmentContainer(ItemContainer container) { List> 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 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 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 gpsMap = new LinkedHashMap<>(); gpsMap.put("x", gpsTarget.getX()); gpsMap.put("y", gpsTarget.getY()); gpsMap.put("plane", gpsTarget.getPlane()); List> pathList = new ArrayList<>(); if (gpsPath != null) { for (WorldPoint p : gpsPath) { Map 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> scList = new ArrayList<>(); if (nearbyShortcuts != null) { for (ShortcutData sc : nearbyShortcuts) { Map 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); } } }