diff --git a/http-api/src/main/java/net/runelite/http/api/hiscore/HiscoreClient.java b/http-api/src/main/java/net/runelite/http/api/hiscore/HiscoreClient.java index 3ec6cb5448..d1d0d25113 100644 --- a/http-api/src/main/java/net/runelite/http/api/hiscore/HiscoreClient.java +++ b/http-api/src/main/java/net/runelite/http/api/hiscore/HiscoreClient.java @@ -56,6 +56,12 @@ public class HiscoreClient try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) { + if (!response.isSuccessful()) + { + logger.debug("unsuccessful lookup for {}", username); + return null; + } + InputStream in = response.body().byteStream(); return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), HiscoreResult.class); } @@ -88,6 +94,12 @@ public class HiscoreClient try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) { + if (!response.isSuccessful()) + { + logger.debug("unsuccessful lookup for {}", username); + return null; + } + InputStream in = response.body().byteStream(); return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), SingleHiscoreSkillResult.class); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java index e8045b7dfe..c669337aa4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java @@ -413,11 +413,7 @@ public class HiscorePanel extends PluginPanel return; } - /* - For some reason, the fetch results would sometimes return a not null object - with all null attributes, to check for that, i'll just null check one of the attributes. - */ - if (result == null || result.getAttack() == null) + if (result == null) { input.setIcon(ERROR_ICON); input.setEditable(true); 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 new file mode 100644 index 0000000000..929fbed33f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018, Jordan Atwood + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.opponentinfo; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "opponentinfo", + name = "Opponent Info", + description = "Configuration for the Opponent info plugin" +) +public interface OpponentInfoConfig extends Config +{ + @ConfigItem( + keyName = "lookupOnInteraction", + name = "Lookup players on interaction", + description = "Display a combat stat comparison panel on player interaction. (follow, trade, challenge, attack, etc.)" + ) + default boolean lookupOnInteraction() + { + 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 400bc39ef5..c558d02ce5 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 @@ -1,5 +1,6 @@ /* - * Copyright (c) 2016-2017, Adam + * Copyright (c) 2016-2018, Adam + * Copyright (c) 2018, Jordan Atwood * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -30,9 +31,6 @@ import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; -import java.time.Duration; -import java.time.Instant; -import java.util.Map; import javax.inject.Inject; import net.runelite.api.Actor; import net.runelite.api.Client; @@ -54,22 +52,17 @@ class OpponentInfoOverlay extends Overlay { private static final Color HP_GREEN = new Color(0, 146, 54, 230); private static final Color HP_RED = new Color(102, 15, 16, 230); - private static final Duration WAIT = Duration.ofSeconds(3); private final Client client; private final OpponentInfoPlugin opponentInfoPlugin; private final HiscoreManager hiscoreManager; - private final NPC[] clientNpcs; private final PanelComponent panelComponent = new PanelComponent(); - private final Map oppInfoHealth = OpponentInfoPlugin.loadNpcHealth(); private Integer lastMaxHealth; private float lastRatio = 0; - private Instant lastTime = Instant.now(); private String opponentName; private String opponentsOpponentName; - private NPC lastOpponent; @Inject private OpponentInfoOverlay(Client client, OpponentInfoPlugin opponentInfoPlugin, HiscoreManager hiscoreManager) @@ -78,7 +71,6 @@ class OpponentInfoOverlay extends Overlay this.opponentInfoPlugin = opponentInfoPlugin; this.hiscoreManager = hiscoreManager; - this.clientNpcs = client.getCachedNPCs(); setPosition(OverlayPosition.TOP_LEFT); setPriority(OverlayPriority.HIGH); @@ -86,58 +78,33 @@ class OpponentInfoOverlay extends Overlay panelComponent.setGap(new Point(0, 2)); } - private Actor getOpponent() - { - Player player = client.getLocalPlayer(); - if (player == null) - { - return null; - } - - return player.getInteracting(); - } - @Override public Dimension render(Graphics2D graphics) { - Actor opponent = getOpponent(); + final Actor opponent = opponentInfoPlugin.getLastOpponent(); - // If opponent is null, try to use last opponent if (opponent == null) { - if (lastOpponent != null && clientNpcs[lastOpponent.getIndex()] != lastOpponent) - { - // lastOpponent is no longer valid - lastOpponent = null; - } - else - { - opponent = lastOpponent; - } - } - else - { - // Update last opponent - lastOpponent = opponent instanceof NPC ? (NPC) opponent : null; + opponentName = null; + return null; } - if (opponent != null && opponent.getHealth() > 0) + if (opponent.getName() != null && opponent.getHealth() > 0) { - lastTime = Instant.now(); lastRatio = (float) opponent.getHealthRatio() / (float) opponent.getHealth(); opponentName = Text.removeTags(opponent.getName()); lastMaxHealth = null; if (opponent instanceof NPC) { - lastMaxHealth = oppInfoHealth.get(opponentName + "_" + opponent.getCombatLevel()); + lastMaxHealth = opponentInfoPlugin.getOppInfoHealth().get(opponentName + "_" + opponent.getCombatLevel()); } else if (opponent instanceof Player) { - HiscoreResult hiscoreResult = hiscoreManager.lookupAsync(opponentName, opponentInfoPlugin.getHiscoreEndpoint()); + final HiscoreResult hiscoreResult = hiscoreManager.lookupAsync(opponentName, opponentInfoPlugin.getHiscoreEndpoint()); if (hiscoreResult != null) { - int hp = hiscoreResult.getHitpoints().getLevel(); + final int hp = hiscoreResult.getHitpoints().getLevel(); if (hp > 0) { lastMaxHealth = hp; @@ -145,7 +112,7 @@ class OpponentInfoOverlay extends Overlay } } - Actor opponentsOpponent = opponent.getInteracting(); + final Actor opponentsOpponent = opponent.getInteracting(); if (opponentsOpponent != null && (opponentsOpponent != client.getLocalPlayer() || client.getVar(Varbits.MULTICOMBAT_AREA) == 1)) { @@ -157,9 +124,9 @@ class OpponentInfoOverlay extends Overlay } } - if (Duration.between(Instant.now(), lastTime).abs().compareTo(WAIT) > 0) + if (opponentName == null) { - return null; //don't draw anything. + return null; } final FontMetrics fontMetrics = graphics.getFontMetrics(); 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 2e008dc245..6f7c98dbf4 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 @@ -1,5 +1,6 @@ /* - * Copyright (c) 2016-2017, Adam + * Copyright (c) 2016-2018, Adam + * Copyright (c) 2018, Jordan Atwood * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,18 +28,25 @@ package net.runelite.client.plugins.opponentinfo; import com.google.common.eventbus.Subscribe; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.google.inject.Provides; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; +import java.time.Duration; +import java.time.Instant; import java.util.EnumSet; import java.util.Map; import javax.inject.Inject; import lombok.AccessLevel; import lombok.Getter; +import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.WorldType; import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.InteractingChanged; +import net.runelite.client.config.ConfigManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.overlay.OverlayManager; @@ -49,28 +57,52 @@ import net.runelite.http.api.hiscore.HiscoreEndpoint; ) public class OpponentInfoPlugin extends Plugin { + private static final Duration WAIT = Duration.ofSeconds(5); + @Inject private Client client; + @Inject + private OpponentInfoConfig config; + @Inject private OverlayManager overlayManager; @Inject - private OpponentInfoOverlay overlay; + private OpponentInfoOverlay opponentInfoOverlay; + + @Inject + private PlayerComparisonOverlay playerComparisonOverlay; @Getter(AccessLevel.PACKAGE) private HiscoreEndpoint hiscoreEndpoint = HiscoreEndpoint.NORMAL; + @Getter(AccessLevel.PACKAGE) + private Actor lastOpponent; + + private Instant lastTime = null; + + @Getter(AccessLevel.PACKAGE) + private final Map oppInfoHealth = loadNpcHealth(); + + @Provides + OpponentInfoConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(OpponentInfoConfig.class); + } + @Override protected void startUp() throws Exception { - overlayManager.add(overlay); + overlayManager.add(opponentInfoOverlay); + overlayManager.add(playerComparisonOverlay); } @Override protected void shutDown() throws Exception { - overlayManager.remove(overlay); + overlayManager.remove(opponentInfoOverlay); + overlayManager.remove(playerComparisonOverlay); } @Subscribe @@ -96,7 +128,38 @@ public class OpponentInfoPlugin extends Plugin } } - public static Map loadNpcHealth() + @Subscribe + public void onInteractingChanged(InteractingChanged event) + { + if (event.getSource() != client.getLocalPlayer()) + { + return; + } + + Actor opponent = event.getTarget(); + + if (opponent == null) + { + lastTime = Instant.now(); + return; + } + + lastOpponent = opponent; + } + + @Subscribe + public void onGameTick(GameTick gameTick) + { + if (lastOpponent != null && client.getLocalPlayer().getInteracting() == null) + { + if (Duration.between(lastTime, Instant.now()).compareTo(WAIT) > 0) + { + lastOpponent = null; + } + } + } + + private Map loadNpcHealth() { Gson gson = new Gson(); Type type = new TypeToken>() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/PlayerComparisonOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/PlayerComparisonOverlay.java new file mode 100644 index 0000000000..bccdbe4588 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/PlayerComparisonOverlay.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2018, Jordan Atwood + * Copyright (c) 2018, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.opponentinfo; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.Player; +import net.runelite.api.Skill; +import net.runelite.client.game.HiscoreManager; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; +import net.runelite.client.util.Text; +import net.runelite.http.api.hiscore.HiscoreResult; +import net.runelite.http.api.hiscore.HiscoreSkill; + +class PlayerComparisonOverlay extends Overlay +{ + private static final Color HIGHER_STAT_TEXT_COLOR = Color.GREEN; + private static final Color LOWER_STAT_TEXT_COLOR = Color.RED; + private static final Color NEUTRAL_TEXT_COLOR = Color.WHITE; + private static final Color HIGHLIGHT_COLOR = new Color(255, 200, 0, 255); + + private static final Skill[] COMBAT_SKILLS = new Skill[]{ + Skill.ATTACK, + Skill.STRENGTH, + Skill.DEFENCE, + Skill.HITPOINTS, + Skill.RANGED, + Skill.MAGIC, + Skill.PRAYER + }; + + private static final HiscoreSkill[] HISCORE_COMBAT_SKILLS = new HiscoreSkill[]{ + HiscoreSkill.ATTACK, + HiscoreSkill.STRENGTH, + HiscoreSkill.DEFENCE, + HiscoreSkill.HITPOINTS, + HiscoreSkill.RANGED, + HiscoreSkill.MAGIC, + HiscoreSkill.PRAYER + }; + + private static final String LEFT_COLUMN_HEADER = "Skill"; + private static final String RIGHT_COLUMN_HEADER = "You/Them"; + + private final Client client; + private final OpponentInfoPlugin opponentInfoPlugin; + private final OpponentInfoConfig config; + private final HiscoreManager hiscoreManager; + private final PanelComponent panelComponent = new PanelComponent(); + + @Inject + private PlayerComparisonOverlay(Client client, OpponentInfoPlugin opponentInfoPlugin, OpponentInfoConfig config, HiscoreManager hiscoreManager) + { + this.client = client; + this.opponentInfoPlugin = opponentInfoPlugin; + this.config = config; + this.hiscoreManager = hiscoreManager; + + setPosition(OverlayPosition.BOTTOM_LEFT); + setLayer(OverlayLayer.ABOVE_WIDGETS); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!config.lookupOnInteraction()) + { + return null; + } + + final Actor opponent = opponentInfoPlugin.getLastOpponent(); + + if (opponent == null) + { + return null; + } + + // Don't try to look up NPC names + if (!(opponent instanceof Player)) + { + return null; + } + + final String opponentName = Text.removeTags(opponent.getName()); + final HiscoreResult hiscoreResult = hiscoreManager.lookupAsync(opponentName, opponentInfoPlugin.getHiscoreEndpoint()); + if (hiscoreResult == null) + { + return null; + } + + panelComponent.getChildren().clear(); + generateComparisonTable(panelComponent, hiscoreResult); + return panelComponent.render(graphics); + } + + private void generateComparisonTable(PanelComponent panelComponent, HiscoreResult opponentSkills) + { + final String opponentName = opponentSkills.getPlayer(); + + panelComponent.getChildren().add( + TitleComponent.builder() + .text(opponentName) + .color(HIGHLIGHT_COLOR) + .build()); + + panelComponent.getChildren().add( + LineComponent.builder() + .left(LEFT_COLUMN_HEADER) + .leftColor(HIGHLIGHT_COLOR) + .right(RIGHT_COLUMN_HEADER) + .rightColor(HIGHLIGHT_COLOR) + .build()); + + for (int i = 0; i < COMBAT_SKILLS.length; ++i) + { + final HiscoreSkill hiscoreSkill = HISCORE_COMBAT_SKILLS[i]; + final Skill skill = COMBAT_SKILLS[i]; + + final net.runelite.http.api.hiscore.Skill opponentSkill = opponentSkills.getSkill(hiscoreSkill); + + if (opponentSkill == null || opponentSkill.getLevel() == -1) + { + continue; + } + + final int playerSkillLevel = client.getRealSkillLevel(skill); + final int opponentSkillLevel = opponentSkill.getLevel(); + + panelComponent.getChildren().add( + LineComponent.builder() + .left(hiscoreSkill.getName()) + .right(Integer.toString(playerSkillLevel) + "/" + Integer.toString(opponentSkillLevel)) + .rightColor(comparisonStatColor(playerSkillLevel, opponentSkillLevel)) + .build()); + } + } + + private static Color comparisonStatColor(int a, int b) + { + if (a > b) + { + return HIGHER_STAT_TEXT_COLOR; + } + if (a < b) + { + return LOWER_STAT_TEXT_COLOR; + } + return NEUTRAL_TEXT_COLOR; + } +}