diff --git a/runelite-api/src/main/java/net/runelite/api/Actor.java b/runelite-api/src/main/java/net/runelite/api/Actor.java index 9723fc9605..79ca20df88 100644 --- a/runelite-api/src/main/java/net/runelite/api/Actor.java +++ b/runelite-api/src/main/java/net/runelite/api/Actor.java @@ -64,8 +64,11 @@ public interface Actor extends Entity, Locatable * * * @return the actor, null if no interaction is occurring + * + * (getRSInteracting returns the npc/player index, useful for menus) */ Actor getInteracting(); + int getRSInteracting(); /** * Gets the health ratio of the actor. diff --git a/runelite-api/src/main/java/net/runelite/api/Client.java b/runelite-api/src/main/java/net/runelite/api/Client.java index d7bd5efcc1..78e0f58eb1 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -341,9 +341,12 @@ public interface Client extends GameShell * Gets the logged in player instance. * * @return the logged in player + * + * (getLocalPlayerIndex returns the local index, useful for menus/interacting) */ @Nullable Player getLocalPlayer(); + int getLocalPlayerIndex(); /** * Gets the item composition corresponding to an items ID. diff --git a/runelite-client/src/main/java/net/runelite/client/menus/MenuManager.java b/runelite-client/src/main/java/net/runelite/client/menus/MenuManager.java index b7a96caa27..0d0e405229 100644 --- a/runelite-client/src/main/java/net/runelite/client/menus/MenuManager.java +++ b/runelite-client/src/main/java/net/runelite/client/menus/MenuManager.java @@ -91,6 +91,8 @@ public class MenuManager private MenuEntry leftClickEntry = null; private MenuEntry firstEntry = null; + private int playerAttackIdx = -1; + @Inject private MenuManager(Client client, EventBus eventBus) { @@ -500,6 +502,29 @@ public class MenuManager return index; } + public int getPlayerAttackOpcode() + { + final String[] playerMenuOptions = client.getPlayerOptions(); + + if (playerAttackIdx != -1 && playerMenuOptions[playerAttackIdx].equals("Attack")) + { + return client.getPlayerMenuTypes()[playerAttackIdx]; + } + + playerAttackIdx = -1; + + for (int i = IDX_LOWER; i < IDX_UPPER; i++) + { + if ("Attack".equals(playerMenuOptions[i])) + { + playerAttackIdx = i; + break; + } + } + + return playerAttackIdx >= 0 ? client.getPlayerMenuTypes()[playerAttackIdx] : -1; + } + /** * Adds to the set of menu entries which when present, will remove all entries except for this one */ @@ -522,7 +547,7 @@ public class MenuManager AbstractComparableEntry entry = newBaseComparableEntry(option, target); - priorityEntries.removeIf(entry::equals); + priorityEntries.remove(entry); } @@ -562,7 +587,7 @@ public class MenuManager public void removePriorityEntry(AbstractComparableEntry entry) { - priorityEntries.removeIf(entry::equals); + priorityEntries.remove(entry); } public void removePriorityEntry(String option) @@ -571,7 +596,7 @@ public class MenuManager AbstractComparableEntry entry = newBaseComparableEntry(option, "", false); - priorityEntries.removeIf(entry::equals); + priorityEntries.remove(entry); } public void removePriorityEntry(String option, boolean strictOption) @@ -581,7 +606,17 @@ public class MenuManager AbstractComparableEntry entry = newBaseComparableEntry(option, "", -1, -1, false, strictOption); - priorityEntries.removeIf(entry::equals); + priorityEntries.remove(entry); + } + + public void addPriorityEntries(Collection entries) + { + priorityEntries.addAll(entries); + } + + public void removePriorityEntries(Collection entries) + { + priorityEntries.removeAll(entries); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoConfig.java index 9f08e128af..af11a32c28 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoConfig.java @@ -24,6 +24,7 @@ */ package net.runelite.client.plugins.opponentinfo; +import java.awt.Color; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; @@ -65,12 +66,47 @@ public interface OpponentInfoConfig extends Config } @ConfigItem( - keyName = "showOpponentsInMenu", - name = "Show opponents in menu", - description = "Marks opponents names in the menu which you are attacking or are attacking you (NPC only)", - position = 3 + keyName = "showAttackersMenu", + name = "Show attackers in menu", + description = "Marks attackers' names in menus with a *
", + position = 3, + warning = "NOTE: This'll also mark people who are following you/interacting with you in any other way. Don't blindly trust this in pvp!" ) - default boolean showOpponentsInMenu() + default boolean showAttackersMenu() + { + return false; + } + + @ConfigItem( + keyName = "showAttackingMenu", + name = "Green main target", + description = "Display main target's name colored in menus (Players and NPCs)", + position = 4, + warning = "NOTE: This'll also show green when following/interacting in any other way. Don't blindly trust this in pvp!" + ) + default boolean showAttackingMenu() + { + return false; + } + + @ConfigItem( + keyName = "attackingColor", + name = "Target color", + description = "The color your target will be highlighted with", + position = 5 + ) + default Color attackingColor() + { + return Color.GREEN; + } + + @ConfigItem( + keyName = "showHitpointsMenu", + name = "Show NPC hp in menu", + description = "Show NPC hp in menu. Useful when barraging", + position = 6 + ) + default boolean showHitpointsMenu() { return false; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java index e304a2cded..8fb33c8164 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java @@ -36,11 +36,7 @@ import javax.inject.Singleton; import net.runelite.api.Actor; import net.runelite.api.Client; import static net.runelite.api.MenuOpcode.RUNELITE_OVERLAY_CONFIG; -import net.runelite.api.NPC; -import net.runelite.api.Player; import net.runelite.api.Varbits; -import net.runelite.client.game.HiscoreManager; -import net.runelite.client.game.NPCManager; import net.runelite.client.ui.overlay.Overlay; import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; import net.runelite.client.ui.overlay.OverlayMenuEntry; @@ -51,7 +47,6 @@ import net.runelite.client.ui.overlay.components.PanelComponent; import net.runelite.client.ui.overlay.components.ProgressBarComponent; import net.runelite.client.ui.overlay.components.TitleComponent; import net.runelite.api.util.Text; -import net.runelite.http.api.hiscore.HiscoreResult; @Singleton class OpponentInfoOverlay extends Overlay @@ -61,8 +56,6 @@ class OpponentInfoOverlay extends Overlay private final Client client; private final OpponentInfoPlugin opponentInfoPlugin; - private final HiscoreManager hiscoreManager; - private final NPCManager npcManager; private final PanelComponent panelComponent = new PanelComponent(); @@ -75,15 +68,11 @@ class OpponentInfoOverlay extends Overlay @Inject private OpponentInfoOverlay( final Client client, - final OpponentInfoPlugin opponentInfoPlugin, - final HiscoreManager hiscoreManager, - final NPCManager npcManager) + final OpponentInfoPlugin opponentInfoPlugin) { super(opponentInfoPlugin); this.client = client; this.opponentInfoPlugin = opponentInfoPlugin; - this.hiscoreManager = hiscoreManager; - this.npcManager = npcManager; setPosition(OverlayPosition.TOP_LEFT); setPriority(OverlayPriority.HIGH); @@ -110,23 +99,7 @@ class OpponentInfoOverlay extends Overlay lastHealthScale = opponent.getHealth(); opponentName = Text.removeTags(opponent.getName()); - lastMaxHealth = -1; - if (opponent instanceof NPC) - { - lastMaxHealth = npcManager.getHealth(((NPC) opponent).getId()); - } - else if (opponent instanceof Player) - { - final HiscoreResult hiscoreResult = hiscoreManager.lookupAsync(opponentName, opponentInfoPlugin.getHiscoreEndpoint()); - if (hiscoreResult != null) - { - final int hp = hiscoreResult.getHitpoints().getLevel(); - if (hp > 0) - { - lastMaxHealth = hp; - } - } - } + lastMaxHealth = opponentInfoPlugin.getMaxHp(opponent); final Actor opponentsOpponent = opponent.getInteracting(); if (opponentsOpponent != null @@ -168,37 +141,7 @@ class OpponentInfoOverlay extends Overlay if ((displayStyle == HitpointsDisplayStyle.HITPOINTS || displayStyle == HitpointsDisplayStyle.BOTH) && lastMaxHealth != -1) { - // This is the reverse of the calculation of healthRatio done by the server - // which is: healthRatio = 1 + (healthScale - 1) * health / maxHealth (if health > 0, 0 otherwise) - // It's able to recover the exact health if maxHealth <= healthScale. - int health = 0; - if (lastRatio > 0) - { - int minHealth = 1; - int maxHealth; - if (lastHealthScale > 1) - { - if (lastRatio > 1) - { - // This doesn't apply if healthRatio = 1, because of the special case in the server calculation that - // health = 0 forces healthRatio = 0 instead of the expected healthRatio = 1 - minHealth = (lastMaxHealth * (lastRatio - 1) + lastHealthScale - 2) / (lastHealthScale - 1); - } - maxHealth = (lastMaxHealth * lastRatio - 1) / (lastHealthScale - 1); - if (maxHealth > lastMaxHealth) - { - maxHealth = lastMaxHealth; - } - } - else - { - // If healthScale is 1, healthRatio will always be 1 unless health = 0 - // so we know nothing about the upper limit except that it can't be higher than maxHealth - maxHealth = lastMaxHealth; - } - // Take the average of min and max possible healths - health = (minHealth + maxHealth + 1) / 2; - } + int health = getExactHp(lastRatio, lastHealthScale, lastMaxHealth); // Show both the hitpoint and percentage values if enabled in the config final ProgressBarComponent.LabelDisplayMode progressBarDisplayMode = displayStyle == HitpointsDisplayStyle.BOTH ? @@ -229,4 +172,47 @@ class OpponentInfoOverlay extends Overlay return panelComponent.render(graphics); } + + static int getExactHp(int ratio, int health, int maxHp) + { + if (ratio < 0 || health <= 0 || maxHp == -1) + { + return -1; + } + + int exactHealth = 0; + + // This is the reverse of the calculation of healthRatio done by the server + // which is: healthRatio = 1 + (healthScale - 1) * health / maxHealth (if health > 0, 0 otherwise) + // It's able to recover the exact health if maxHealth <= healthScale. + if (ratio > 0) + { + int minHealth = 1; + int maxHealth; + if (health > 1) + { + if (ratio > 1) + { + // This doesn't apply if healthRatio = 1, because of the special case in the server calculation that + // health = 0 forces healthRatio = 0 instead of the expected healthRatio = 1 + minHealth = (maxHp * (ratio - 1) + health - 2) / (health - 1); + } + maxHealth = (maxHp * ratio - 1) / (health - 1); + if (maxHealth > maxHp) + { + maxHealth = maxHp; + } + } + else + { + // If healthScale is 1, healthRatio will always be 1 unless health = 0 + // so we know nothing about the upper limit except that it can't be higher than maxHealth + maxHealth = maxHp; + } + // Take the average of min and max possible healths + exactHealth = (minHealth + maxHealth + 1) / 2; + } + + return exactHealth; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java index f6f63b08d3..26ac5ba219 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java @@ -33,24 +33,33 @@ import javax.inject.Inject; import javax.inject.Singleton; import lombok.AccessLevel; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.MenuEntry; import net.runelite.api.MenuOpcode; import net.runelite.api.NPC; +import net.runelite.api.Player; import net.runelite.api.WorldType; +import net.runelite.api.events.BeforeRender; import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; import net.runelite.api.events.InteractingChanged; -import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.events.MenuOpened; +import net.runelite.api.util.Text; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.EventBus; +import net.runelite.client.game.HiscoreManager; +import net.runelite.client.game.NPCManager; +import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.util.ColorUtil; import net.runelite.http.api.hiscore.HiscoreEndpoint; +import net.runelite.http.api.hiscore.HiscoreResult; @PluginDescriptor( name = "Opponent Information", @@ -58,9 +67,12 @@ import net.runelite.http.api.hiscore.HiscoreEndpoint; tags = {"combat", "health", "hitpoints", "npcs", "overlay"} ) @Singleton +@Slf4j public class OpponentInfoPlugin extends Plugin { private static final Duration WAIT = Duration.ofSeconds(5); + private static final Object MENU = new Object(); + private static final int COLOR_TAG_LENGTH = 12; @Inject private Client client; @@ -80,6 +92,15 @@ public class OpponentInfoPlugin extends Plugin @Inject private EventBus eventBus; + @Inject + private HiscoreManager hiscoreManager; + + @Inject + private MenuManager menuManager; + + @Inject + private NPCManager npcManager; + @Getter(AccessLevel.PACKAGE) private HiscoreEndpoint hiscoreEndpoint = HiscoreEndpoint.NORMAL; @@ -95,6 +116,11 @@ public class OpponentInfoPlugin extends Plugin @Getter(AccessLevel.PACKAGE) private boolean showOpponentsOpponent; + private String attackingColTag; + private boolean showAttackers; + private boolean showAttacking; + private boolean showHitpoints; + @Provides OpponentInfoConfig provideConfig(ConfigManager configManager) { @@ -104,6 +130,11 @@ public class OpponentInfoPlugin extends Plugin @Override protected void startUp() throws Exception { + this.attackingColTag = ColorUtil.colorTag(config.attackingColor()); + this.showAttackers = config.showAttackersMenu(); + this.showAttacking = config.showAttackingMenu(); + this.showHitpoints = config.showHitpointsMenu(); + updateConfig(); addSubscriptions(); @@ -115,6 +146,7 @@ public class OpponentInfoPlugin extends Plugin protected void shutDown() throws Exception { eventBus.unregister(this); + eventBus.unregister(MENU); lastOpponent = null; lastTime = null; @@ -128,7 +160,20 @@ public class OpponentInfoPlugin extends Plugin eventBus.subscribe(GameStateChanged.class, this, this::onGameStateChanged); eventBus.subscribe(InteractingChanged.class, this, this::onInteractingChanged); eventBus.subscribe(GameTick.class, this, this::onGameTick); - eventBus.subscribe(MenuEntryAdded.class, this, this::onMenuEntryAdded); + updateMenuSubs(); + } + + private void updateMenuSubs() + { + if (showAttackers || showAttacking || showHitpoints) + { + eventBus.subscribe(BeforeRender.class, MENU, this::onBeforeRender); + eventBus.subscribe(MenuOpened.class, MENU, this::onMenuOpened); + } + else + { + eventBus.unregister(MENU); + } } private void onGameStateChanged(GameStateChanged gameStateChanged) @@ -194,6 +239,25 @@ public class OpponentInfoPlugin extends Plugin return; } + switch (event.getKey()) + { + case "showAttackersMenu": + this.showAttackers = config.showAttackersMenu(); + updateMenuSubs(); + break; + case "showAttackingMenu": + this.showAttacking = config.showAttackingMenu(); + updateMenuSubs(); + break; + case "showHitpointsMenu": + this.showHitpoints = config.showHitpointsMenu(); + updateMenuSubs(); + break; + case "attackingColor": + attackingColTag = ColorUtil.colorTag(config.attackingColor()); + break; + } + updateConfig(); } @@ -204,27 +268,253 @@ public class OpponentInfoPlugin extends Plugin this.showOpponentsOpponent = config.showOpponentsOpponent(); } - private void onMenuEntryAdded(MenuEntryAdded menuEntryAdded) + private void onBeforeRender(BeforeRender event) { - if (menuEntryAdded.getOpcode() != MenuOpcode.NPC_SECOND_OPTION.getId() - || !menuEntryAdded.getOption().equals("Attack") - || !config.showOpponentsInMenu()) + if (client.getMenuOptionCount() <= 0) { return; } - - int npcIndex = menuEntryAdded.getIdentifier(); - NPC npc = client.getCachedNPCs()[npcIndex]; - if (npc == null) + if (client.isMenuOpen()) { + boolean changed = false; + final MenuEntry[] entries = client.getMenuEntries(); + for (final MenuEntry entry : entries) + { + changed |= fixup(entry); + } + + if (changed) + { + client.setMenuEntries(entries); + } return; } - if (npc.getInteracting() == client.getLocalPlayer() || lastOpponent == npc) + final MenuEntry entry = client.getLeftClickMenuEntry(); + if (modify(entry)) { - MenuEntry[] menuEntries = client.getMenuEntries(); - menuEntries[menuEntries.length - 1].setTarget("*" + menuEntryAdded.getTarget()); - client.setMenuEntries(menuEntries); + client.setLeftClickMenuEntry(entry); + } + } + + private void onMenuOpened(MenuOpened event) + { + boolean changed = false; + for (MenuEntry entry : event.getMenuEntries()) + { + changed |= modify(entry); + } + + if (changed) + { + event.setModified(); + } + } + + private boolean modify(MenuEntry entry) + { + if (isNotAttackEntry(entry)) + { + return false; + } + + boolean changed = false; + + int index = entry.getIdentifier(); + Actor actor = getActorFromIndex(index); + + if (actor == null) + { + return false; + } + + if (actor instanceof Player) + { + index -= 32768; + } + + String target = entry.getTarget(); + + if (showAttacking && + client.getLocalPlayer().getRSInteracting() == index) + { + target = attackingColTag + target.substring(COLOR_TAG_LENGTH); + changed = true; + } + + if (showAttackers && + actor.getRSInteracting() - 32768 == client.getLocalPlayerIndex()) + { + target = '*' + target; + changed = true; + } + + if (showHitpoints && + actor.getHealth() > 0) + { + int lvlIndex = target.lastIndexOf("(level-"); + if (lvlIndex != -1) + { + String levelReplacement = getHpString(actor, true); + + target = target.substring(0, lvlIndex) + levelReplacement; + changed = true; + } + } + + if (changed) + { + entry.setTarget(target); + return true; + } + + return false; + } + + private boolean fixup(MenuEntry entry) + { + if (isNotAttackEntry(entry)) + { + return false; + } + + int index = entry.getIdentifier(); + + Actor actor = getActorFromIndex(index); + + if (actor == null) + { + return false; + } + + if (actor instanceof Player) + { + index -= 32768; + } + + String target = entry.getTarget(); + + boolean hasAggro = actor.getRSInteracting() - 32768 == client.getLocalPlayerIndex(); + boolean hadAggro = target.charAt(0) == '*'; + boolean isTarget = client.getLocalPlayer().getRSInteracting() == index; + boolean hasHp = showHitpoints && actor instanceof NPC && actor.getHealth() > 0; + + boolean aggroChanged = showAttackers && hasAggro != hadAggro; + boolean targetChanged = showAttacking && isTarget != target.startsWith(attackingColTag, aggroChanged ? 1 : 0); + boolean hpChanged = showHitpoints && hasHp == target.contains("(level-"); + + if (!aggroChanged && + !targetChanged && + !hasHp && + !hpChanged) + { + return false; + } + + if (targetChanged) + { + boolean player = actor instanceof Player; + final int start = hadAggro ? 1 + COLOR_TAG_LENGTH : COLOR_TAG_LENGTH; + target = + (hasAggro ? '*' : "") + + (isTarget ? attackingColTag : + player ? ColorUtil.colorStartTag(0xffffff) : ColorUtil.colorStartTag(0xffff00)) + + target.substring(start); + } + else if (aggroChanged) + { + if (hasAggro) + { + target = '*' + target; + } + else + { + target = target.substring(1); + } + } + + if (hpChanged || hasHp) + { + final int braceIdx; + + if (!hasHp) + { + braceIdx = target.lastIndexOf("("); + if (braceIdx != -1) + { + target = target.substring(0, braceIdx - 1) + "(level-" + actor.getCombatLevel() + ")"; + } + } + else if ((braceIdx = target.lastIndexOf('(')) != -1) + { + final String hpString = getHpString(actor, hpChanged); + + target = target.substring(0, braceIdx) + hpString; + } + } + + entry.setTarget(target); + return true; + } + + private boolean isNotAttackEntry(MenuEntry entry) + { + return entry.getOpcode() != MenuOpcode.NPC_SECOND_OPTION.getId() && + entry.getOpcode() != menuManager.getPlayerAttackOpcode() + || !entry.getOption().equals("Attack"); + } + + private String getHpString(Actor actor, boolean withColorTag) + { + int maxHp = getMaxHp(actor); + int health = actor.getHealth(); + int ratio = actor.getHealthRatio(); + + final String result; + if (maxHp != -1) + { + final int exactHealth = OpponentInfoOverlay.getExactHp(ratio, health, maxHp); + result = "(" + exactHealth + "/" + maxHp + ")"; + } + else + { + result = "(" + (100 * ratio) / health + "%)"; + } + + return withColorTag ? ColorUtil.colorStartTag(0xff0000) + result : result; + } + + int getMaxHp(Actor actor) + { + if (actor instanceof NPC) + { + return npcManager.getHealth(((NPC) actor).getId()); + } + else + { + final HiscoreResult hiscoreResult = hiscoreManager.lookupAsync(Text.removeTags(actor.getName()), getHiscoreEndpoint()); + if (hiscoreResult != null) + { + final int hp = hiscoreResult.getHitpoints().getLevel(); + if (hp > 0) + { + return hp; + } + } + + return -1; + } + } + + private Actor getActorFromIndex(int index) + { + if (index < 32768) + { + return client.getCachedNPCs()[index]; + } + else + { + return client.getCachedPlayers()[index - 32768]; } } } diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java b/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java index 8a5ffc4b6e..00dae893e9 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java @@ -30,6 +30,7 @@ import net.runelite.mapping.Import; public interface RSActor extends RSEntity, Actor { @Import("targetIndex") + @Override int getRSInteracting(); // Overhead text diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java index a0e6506b08..5ecdfc616b 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java @@ -208,6 +208,10 @@ public interface RSClient extends RSGameShell, Client @Override RSPlayer getLocalPlayer(); + @Import("localPlayerIndex") + @Override + int getLocalPlayerIndex(); + @Import("npcCount") int getNpcIndexesCount();