From 3248f226506962bd24f41aa5ec8615d3239fd596 Mon Sep 17 00:00:00 2001 From: therealunull Date: Mon, 14 Dec 2020 05:58:02 -0500 Subject: [PATCH] develop --- .../main/java/net/runelite/api/MenuEntry.java | 10 + .../barrows/BarrowsBrotherSlainOverlay.java | 124 ++ .../plugins/barrows/BarrowsBrothers.java | 48 + .../client/plugins/barrows/BarrowsConfig.java | 100 + .../plugins/barrows/BarrowsOverlay.java | 109 + .../client/plugins/barrows/BarrowsPlugin.java | 256 +++ .../client/plugins/blastfurnace/BarsOres.java | 82 + .../BlastFurnaceClickBoxOverlay.java | 119 ++ .../BlastFurnaceCofferOverlay.java | 96 + .../blastfurnace/BlastFurnaceConfig.java | 66 + .../blastfurnace/BlastFurnaceOverlay.java | 88 + .../blastfurnace/BlastFurnacePlugin.java | 190 ++ .../plugins/blastfurnace/ForemanTimer.java | 42 + .../blastmine/BlastMineOreCountOverlay.java | 94 + .../plugins/blastmine/BlastMinePlugin.java | 135 ++ .../blastmine/BlastMinePluginConfig.java | 101 + .../plugins/blastmine/BlastMineRock.java | 62 + .../blastmine/BlastMineRockOverlay.java | 212 ++ .../plugins/blastmine/BlastMineRockType.java | 69 + .../client/plugins/boosts/BoostIndicator.java | 98 + .../client/plugins/boosts/BoostsConfig.java | 125 ++ .../client/plugins/boosts/BoostsOverlay.java | 141 ++ .../client/plugins/boosts/BoostsPlugin.java | 396 ++++ .../plugins/boosts/StatChangeIndicator.java | 66 + .../client/plugins/bosstimer/Boss.java | 108 + .../plugins/bosstimer/BossTimersPlugin.java | 86 + .../plugins/bosstimer/RespawnTimer.java | 46 + .../client/plugins/cannon/CannonConfig.java | 107 + .../client/plugins/cannon/CannonCounter.java | 52 + .../client/plugins/cannon/CannonOverlay.java | 141 ++ .../client/plugins/cannon/CannonPlugin.java | 420 ++++ .../plugins/cannon/CannonSpotOverlay.java | 117 ++ .../client/plugins/cannon/CannonSpots.java | 93 + .../ChatboxPerformancePlugin.java | 160 ++ .../chatcommands/ChatCommandsConfig.java | 190 ++ .../chatcommands/ChatCommandsPlugin.java | 1846 +++++++++++++++++ .../chatcommands/ChatKeyboardListener.java | 107 + .../chatcommands/SkillAbbreviations.java | 92 + .../plugins/chatfilter/ChatFilterConfig.java | 167 ++ .../plugins/chatfilter/ChatFilterPlugin.java | 386 ++++ .../plugins/chatfilter/ChatFilterType.java | 32 + .../chathistory/ChatHistoryConfig.java | 77 + .../chathistory/ChatHistoryPlugin.java | 425 ++++ .../plugins/chathistory/ChatboxTab.java | 94 + .../ChatNotificationsConfig.java | 111 + .../ChatNotificationsPlugin.java | 331 +++ .../combatlevel/CombatLevelConfig.java | 63 + .../combatlevel/CombatLevelOverlay.java | 136 ++ .../combatlevel/CombatLevelPlugin.java | 244 +++ .../mousehighlight/MouseHighlightConfig.java | 66 + .../mousehighlight/MouseHighlightOverlay.java | 174 ++ .../mousehighlight/MouseHighlightPlugin.java | 64 + 52 files changed, 8764 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrotherSlainOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrothers.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceClickBoxOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnacePlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/ForemanTimer.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePluginConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRock.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/RespawnTimer.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonCounter.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpotOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpots.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatKeyboardListener.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/SkillAbbreviations.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatboxTab.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightPlugin.java diff --git a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java index d0bddc7086..d224e6b183 100644 --- a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java +++ b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java @@ -104,6 +104,11 @@ public class MenuEntry implements Cloneable return this.actionParam; } + public int getParam0() + { + return this.actionParam; + } + public void setParam0(int i) { this.actionParam = i; @@ -114,6 +119,11 @@ public class MenuEntry implements Cloneable this.actionParam1 = i; } + public int getParam1() + { + return this.actionParam1; + } + public void setType(int i) { this.opcode = i; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrotherSlainOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrotherSlainOverlay.java new file mode 100644 index 0000000000..22103f5e79 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrotherSlainOverlay.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2018, Seth + * 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.barrows; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.Client; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import net.runelite.api.Varbits; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.components.LineComponent; + +public class BarrowsBrotherSlainOverlay extends OverlayPanel +{ + private final Client client; + + @Inject + private BarrowsBrotherSlainOverlay(BarrowsPlugin plugin, Client client) + { + super(plugin); + setPosition(OverlayPosition.TOP_LEFT); + setPriority(OverlayPriority.LOW); + this.client = client; + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "Barrows overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + // Do not display overlay if potential is null/hidden + final Widget potential = client.getWidget(WidgetInfo.BARROWS_POTENTIAL); + if (potential == null || potential.isHidden()) + { + return null; + } + + // Hide original overlay + final Widget barrowsBrothers = client.getWidget(WidgetInfo.BARROWS_BROTHERS); + if (barrowsBrothers != null) + { + barrowsBrothers.setHidden(true); + potential.setHidden(true); + } + + for (BarrowsBrothers brother : BarrowsBrothers.values()) + { + final boolean brotherSlain = client.getVar(brother.getKilledVarbit()) > 0; + String slain = brotherSlain ? "\u2713" : "\u2717"; + panelComponent.getChildren().add(LineComponent.builder() + .left(brother.getName()) + .right(slain) + .rightColor(brotherSlain ? Color.GREEN : Color.RED) + .build()); + } + + final int rewardPotential = rewardPotential(); + float rewardPercent = rewardPotential / 10.12f; + panelComponent.getChildren().add(LineComponent.builder() + .left("Potential") + .right(rewardPercent != 0 ? rewardPercent + "%" : "0%") + .rightColor(rewardPotential >= 756 && rewardPotential < 881 ? Color.GREEN : rewardPotential < 631 ? Color.WHITE : Color.YELLOW) + .build()); + + return super.render(graphics); + } + + /** + * Compute the barrows reward potential. Potential rewards are based off of the amount of + * potential. + *

+ * The reward potential thresholds are as follows: + * Mind rune - 381 + * Chaos rune - 506 + * Death rune - 631 + * Blood rune - 756 + * Bolt rack - 881 + * Half key - 1006 + * Dragon med - 1012 + * + * @return potential, 0-1012 inclusive + * @see source + */ + private int rewardPotential() + { + // this is from [proc,barrows_overlay_reward] + int brothers = client.getVar(Varbits.BARROWS_KILLED_AHRIM) + + client.getVar(Varbits.BARROWS_KILLED_DHAROK) + + client.getVar(Varbits.BARROWS_KILLED_GUTHAN) + + client.getVar(Varbits.BARROWS_KILLED_KARIL) + + client.getVar(Varbits.BARROWS_KILLED_TORAG) + + client.getVar(Varbits.BARROWS_KILLED_VERAC); + return client.getVar(Varbits.BARROWS_REWARD_POTENTIAL) + brothers * 2; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrothers.java b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrothers.java new file mode 100644 index 0000000000..6b2f3edab4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsBrothers.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018, Seth + * 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.barrows; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Varbits; +import net.runelite.api.coords.WorldPoint; + +@RequiredArgsConstructor +public enum BarrowsBrothers +{ + AHRIM("Ahrim", new WorldPoint(3566, 3289, 0), Varbits.BARROWS_KILLED_AHRIM), + DHAROK("Dharok", new WorldPoint(3575, 3298, 0), Varbits.BARROWS_KILLED_DHAROK), + GUTHAN("Guthan", new WorldPoint(3577, 3283, 0), Varbits.BARROWS_KILLED_GUTHAN), + KARIL("Karil", new WorldPoint(3566, 3275, 0), Varbits.BARROWS_KILLED_KARIL), + TORAG("Torag", new WorldPoint(3553, 3283, 0), Varbits.BARROWS_KILLED_TORAG), + VERAC("Verac", new WorldPoint(3557, 3298, 0), Varbits.BARROWS_KILLED_VERAC); + + @Getter + private final String name; + @Getter + private final WorldPoint location; + @Getter + private final Varbits killedVarbit; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsConfig.java new file mode 100644 index 0000000000..fe6a657e5e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsConfig.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2018, Seth + * 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.barrows; + +import java.awt.Color; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("barrows") +public interface BarrowsConfig extends Config +{ + @ConfigItem( + keyName = "showBrotherLoc", + name = "Show Brothers location", + description = "Configures whether or not the brothers location is displayed", + position = 1 + ) + default boolean showBrotherLoc() + { + return true; + } + + @ConfigItem( + keyName = "showChestValue", + name = "Show Value of Chests", + description = "Configure whether to show total exchange value of chest when opened", + position = 2 + ) + default boolean showChestValue() + { + return true; + } + + @ConfigItem( + keyName = "brotherLocColor", + name = "Brother location color", + description = "Change the color of the name displayed on the minimap", + position = 3 + ) + default Color brotherLocColor() + { + return Color.CYAN; + } + + @ConfigItem( + keyName = "deadBrotherLocColor", + name = "Dead Brother loc. color", + description = "Change the color of the name displayed on the minimap for a dead brother", + position = 4 + ) + default Color deadBrotherLocColor() + { + return Color.RED; + } + + @ConfigItem( + keyName = "showPuzzleAnswer", + name = "Show Puzzle Answer", + description = "Configures if the puzzle answer should be shown.", + position = 5 + ) + default boolean showPuzzleAnswer() + { + return true; + } + + @ConfigItem( + keyName = "showPrayerDrainTimer", + name = "Show Prayer Drain Timer", + description = "Configure whether or not a countdown until the next prayer drain is displayed", + position = 6 + ) + default boolean showPrayerDrainTimer() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsOverlay.java new file mode 100644 index 0000000000..686cecbf2a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsOverlay.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018, Seth + * 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.barrows; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.widgets.Widget; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; + +class BarrowsOverlay extends Overlay +{ + private final Client client; + private final BarrowsPlugin plugin; + private final BarrowsConfig config; + + @Inject + private BarrowsOverlay(Client client, BarrowsPlugin plugin, BarrowsConfig config) + { + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_WIDGETS); + this.client = client; + this.plugin = plugin; + this.config = config; + } + + @Override + public Dimension render(Graphics2D graphics) + { + Widget puzzleAnswer = plugin.getPuzzleAnswer(); + + if (config.showBrotherLoc()) + { + renderBarrowsBrothers(graphics); + } + + if (puzzleAnswer != null && config.showPuzzleAnswer() && !puzzleAnswer.isHidden()) + { + Rectangle answerRect = puzzleAnswer.getBounds(); + graphics.setColor(Color.GREEN); + graphics.draw(answerRect); + } + + return null; + } + + private void renderBarrowsBrothers(Graphics2D graphics) + { + for (BarrowsBrothers brother : BarrowsBrothers.values()) + { + LocalPoint localLocation = LocalPoint.fromWorld(client, brother.getLocation()); + + if (localLocation == null) + { + continue; + } + + String brotherLetter = Character.toString(brother.getName().charAt(0)); + net.runelite.api.Point minimapText = Perspective.getCanvasTextMiniMapLocation(client, graphics, + localLocation, brotherLetter); + + if (minimapText != null) + { + graphics.setColor(Color.black); + graphics.drawString(brotherLetter, minimapText.getX() + 1, minimapText.getY() + 1); + + if (client.getVar(brother.getKilledVarbit()) > 0) + { + graphics.setColor(config.deadBrotherLocColor()); + } + else + { + graphics.setColor(config.brotherLocColor()); + } + + graphics.drawString(brotherLetter, minimapText.getX(), minimapText.getY()); + } + } + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsPlugin.java new file mode 100644 index 0000000000..149a7ac24e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/barrows/BarrowsPlugin.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2018, Seth + * 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.barrows; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Provides; +import java.time.temporal.ChronoUnit; +import javax.inject.Inject; +import lombok.Getter; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; +import net.runelite.api.ItemContainer; +import net.runelite.api.Player; +import net.runelite.api.SpriteID; +import net.runelite.client.events.ConfigChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.WidgetLoaded; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetID; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.chat.ChatColorType; +import net.runelite.client.chat.ChatMessageBuilder; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.SpriteManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxPriority; +import net.runelite.client.ui.overlay.infobox.LoopTimer; +import net.runelite.client.util.QuantityFormatter; + +@PluginDescriptor( + name = "Barrows Brothers", + description = "Show helpful information for the Barrows minigame", + tags = {"combat", "minigame", "bosses", "pve", "pvm"} +) +public class BarrowsPlugin extends Plugin +{ + private static final ImmutableList POSSIBLE_SOLUTIONS = ImmutableList.of( + WidgetInfo.BARROWS_PUZZLE_ANSWER1, + WidgetInfo.BARROWS_PUZZLE_ANSWER2, + WidgetInfo.BARROWS_PUZZLE_ANSWER3 + ); + + private static final long PRAYER_DRAIN_INTERVAL_MS = 18200; + private static final int CRYPT_REGION_ID = 14231; + + private LoopTimer barrowsPrayerDrainTimer; + private boolean wasInCrypt = false; + + @Getter + private Widget puzzleAnswer; + + @Inject + private OverlayManager overlayManager; + + @Inject + private BarrowsOverlay barrowsOverlay; + + @Inject + private BarrowsBrotherSlainOverlay brotherOverlay; + + @Inject + private Client client; + + @Inject + private ItemManager itemManager; + + @Inject + private SpriteManager spriteManager; + + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private BarrowsConfig config; + + @Provides + BarrowsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(BarrowsConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(barrowsOverlay); + overlayManager.add(brotherOverlay); + } + + @Override + protected void shutDown() + { + overlayManager.remove(barrowsOverlay); + overlayManager.remove(brotherOverlay); + puzzleAnswer = null; + wasInCrypt = false; + stopPrayerDrainTimer(); + + // Restore widgets + final Widget potential = client.getWidget(WidgetInfo.BARROWS_POTENTIAL); + if (potential != null) + { + potential.setHidden(false); + } + + final Widget barrowsBrothers = client.getWidget(WidgetInfo.BARROWS_BROTHERS); + if (barrowsBrothers != null) + { + barrowsBrothers.setHidden(false); + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals("barrows") && !config.showPrayerDrainTimer()) + { + stopPrayerDrainTimer(); + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOADING) + { + wasInCrypt = isInCrypt(); + // on region changes the tiles get set to null + puzzleAnswer = null; + } + else if (event.getGameState() == GameState.LOGGED_IN) + { + boolean isInCrypt = isInCrypt(); + if (wasInCrypt && !isInCrypt) + { + stopPrayerDrainTimer(); + } + else if (!wasInCrypt && isInCrypt) + { + startPrayerDrainTimer(); + } + } + } + + @Subscribe + public void onWidgetLoaded(WidgetLoaded event) + { + if (event.getGroupId() == WidgetID.BARROWS_REWARD_GROUP_ID && config.showChestValue()) + { + ItemContainer barrowsRewardContainer = client.getItemContainer(InventoryID.BARROWS_REWARD); + Item[] items = barrowsRewardContainer.getItems(); + long chestPrice = 0; + + for (Item item : items) + { + long itemStack = (long) itemManager.getItemPrice(item.getId()) * (long) item.getQuantity(); + chestPrice += itemStack; + } + + final ChatMessageBuilder message = new ChatMessageBuilder() + .append(ChatColorType.HIGHLIGHT) + .append("Your chest is worth around ") + .append(QuantityFormatter.formatNumber(chestPrice)) + .append(" coins.") + .append(ChatColorType.NORMAL); + + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.ITEM_EXAMINE) + .runeLiteFormattedMessage(message.build()) + .build()); + } + else if (event.getGroupId() == WidgetID.BARROWS_PUZZLE_GROUP_ID) + { + final int answer = client.getWidget(WidgetInfo.BARROWS_FIRST_PUZZLE).getModelId() - 3; + puzzleAnswer = null; + + for (WidgetInfo puzzleNode : POSSIBLE_SOLUTIONS) + { + final Widget widgetToCheck = client.getWidget(puzzleNode); + + if (widgetToCheck != null && widgetToCheck.getModelId() == answer) + { + puzzleAnswer = client.getWidget(puzzleNode); + break; + } + } + } + } + + private void startPrayerDrainTimer() + { + if (config.showPrayerDrainTimer()) + { + final LoopTimer loopTimer = new LoopTimer( + PRAYER_DRAIN_INTERVAL_MS, + ChronoUnit.MILLIS, + null, + this, + true); + + spriteManager.getSpriteAsync(SpriteID.TAB_PRAYER, 0, loopTimer); + + loopTimer.setPriority(InfoBoxPriority.MED); + loopTimer.setTooltip("Prayer Drain"); + + infoBoxManager.addInfoBox(loopTimer); + barrowsPrayerDrainTimer = loopTimer; + } + } + + private void stopPrayerDrainTimer() + { + infoBoxManager.removeInfoBox(barrowsPrayerDrainTimer); + barrowsPrayerDrainTimer = null; + } + + private boolean isInCrypt() + { + Player localPlayer = client.getLocalPlayer(); + return localPlayer != null && localPlayer.getWorldLocation().getRegionID() == CRYPT_REGION_ID; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java new file mode 100644 index 0000000000..52a295afd3 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018, Seth + * 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.blastfurnace; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import lombok.Getter; +import net.runelite.api.ItemID; +import net.runelite.api.Varbits; + +public enum BarsOres +{ + COPPER_ORE(Varbits.BLAST_FURNACE_COPPER_ORE, ItemID.COPPER_ORE), + TIN_ORE(Varbits.BLAST_FURNACE_TIN_ORE, ItemID.TIN_ORE), + IRON_ORE(Varbits.BLAST_FURNACE_IRON_ORE, ItemID.IRON_ORE), + COAL(Varbits.BLAST_FURNACE_COAL, ItemID.COAL), + MITHRIL_ORE(Varbits.BLAST_FURNACE_MITHRIL_ORE, ItemID.MITHRIL_ORE), + ADAMANTITE_ORE(Varbits.BLAST_FURNACE_ADAMANTITE_ORE, ItemID.ADAMANTITE_ORE), + RUNITE_ORE(Varbits.BLAST_FURNACE_RUNITE_ORE, ItemID.RUNITE_ORE), + SILVER_ORE(Varbits.BLAST_FURNACE_SILVER_ORE, ItemID.SILVER_ORE), + GOLD_ORE(Varbits.BLAST_FURNACE_GOLD_ORE, ItemID.GOLD_ORE), + BRONZE_BAR(Varbits.BLAST_FURNACE_BRONZE_BAR, ItemID.BRONZE_BAR), + IRON_BAR(Varbits.BLAST_FURNACE_IRON_BAR, ItemID.IRON_BAR), + STEEL_BAR(Varbits.BLAST_FURNACE_STEEL_BAR, ItemID.STEEL_BAR), + MITHRIL_BAR(Varbits.BLAST_FURNACE_MITHRIL_BAR, ItemID.MITHRIL_BAR), + ADAMANTITE_BAR(Varbits.BLAST_FURNACE_ADAMANTITE_BAR, ItemID.ADAMANTITE_BAR), + RUNITE_BAR(Varbits.BLAST_FURNACE_RUNITE_BAR, ItemID.RUNITE_BAR), + SILVER_BAR(Varbits.BLAST_FURNACE_SILVER_BAR, ItemID.SILVER_BAR), + GOLD_BAR(Varbits.BLAST_FURNACE_GOLD_BAR, ItemID.GOLD_BAR); + + private static final Map VARBIT; + + static + { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + + for (BarsOres s : values()) + { + builder.put(s.getVarbit(), s); + } + + VARBIT = builder.build(); + } + + @Getter + private final Varbits varbit; + @Getter + private final int itemID; + + BarsOres(Varbits varbit, int itemID) + { + this.varbit = varbit; + this.itemID = itemID; + } + + public static BarsOres getVarbit(Varbits varbit) + { + return VARBIT.get(varbit); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceClickBoxOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceClickBoxOverlay.java new file mode 100644 index 0000000000..01c00107a0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceClickBoxOverlay.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018, Seth + * 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.blastfurnace; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Shape; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.InventoryID; +import net.runelite.api.ItemContainer; +import net.runelite.api.ItemID; +import net.runelite.api.Point; +import net.runelite.api.Varbits; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; + +class BlastFurnaceClickBoxOverlay extends Overlay +{ + private static final int MAX_DISTANCE = 2350; + + private final Client client; + private final BlastFurnacePlugin plugin; + private final BlastFurnaceConfig config; + + @Inject + private BlastFurnaceClickBoxOverlay(Client client, BlastFurnacePlugin plugin, BlastFurnaceConfig config) + { + setPosition(OverlayPosition.DYNAMIC); + this.client = client; + this.plugin = plugin; + this.config = config; + } + + @Override + public Dimension render(Graphics2D graphics) + { + int dispenserState = client.getVar(Varbits.BAR_DISPENSER); + + if (config.showConveyorBelt() && plugin.getConveyorBelt() != null) + { + Color color = dispenserState == 1 ? Color.RED : Color.GREEN; + renderObject(plugin.getConveyorBelt(), graphics, color); + } + + if (config.showBarDispenser() && plugin.getBarDispenser() != null) + { + boolean hasIceGloves = hasIceGloves(); + Color color = dispenserState == 2 && hasIceGloves ? Color.GREEN : (dispenserState == 3 ? Color.GREEN : Color.RED); + + renderObject(plugin.getBarDispenser(), graphics, color); + } + + return null; + } + + private boolean hasIceGloves() + { + ItemContainer equipmentContainer = client.getItemContainer(InventoryID.EQUIPMENT); + if (equipmentContainer == null) + { + return false; + } + + return equipmentContainer.contains(ItemID.ICE_GLOVES); + } + + private void renderObject(GameObject object, Graphics2D graphics, Color color) + { + LocalPoint localLocation = client.getLocalPlayer().getLocalLocation(); + Point mousePosition = client.getMouseCanvasPosition(); + + LocalPoint location = object.getLocalLocation(); + + if (localLocation.distanceTo(location) <= MAX_DISTANCE) + { + Shape objectClickbox = object.getClickbox(); + if (objectClickbox != null) + { + if (objectClickbox.contains(mousePosition.getX(), mousePosition.getY())) + { + graphics.setColor(color.darker()); + } + else + { + graphics.setColor(color); + } + graphics.draw(objectClickbox); + graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20)); + graphics.fill(objectClickbox); + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java new file mode 100644 index 0000000000..1b93f8f640 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018, Seth + * 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.blastfurnace; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.Client; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import static net.runelite.api.Varbits.BLAST_FURNACE_COFFER; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.util.QuantityFormatter; +import static org.apache.commons.lang3.time.DurationFormatUtils.formatDuration; + +class BlastFurnaceCofferOverlay extends OverlayPanel +{ + private static final float COST_PER_HOUR = 72000.0f; + + private final Client client; + private final BlastFurnacePlugin plugin; + private final BlastFurnaceConfig config; + + @Inject + private BlastFurnaceCofferOverlay(Client client, BlastFurnacePlugin plugin, BlastFurnaceConfig config) + { + super(plugin); + setPosition(OverlayPosition.TOP_LEFT); + this.client = client; + this.plugin = plugin; + this.config = config; + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "Coffer overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (plugin.getConveyorBelt() == null) + { + return null; + } + + Widget sack = client.getWidget(WidgetInfo.BLAST_FURNACE_COFFER); + + if (sack != null) + { + final int coffer = client.getVar(BLAST_FURNACE_COFFER); + + sack.setHidden(true); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Coffer:") + .right(QuantityFormatter.quantityToStackSize(coffer) + " gp") + .build()); + + if (config.showCofferTime()) + { + final long millis = (long) (coffer / COST_PER_HOUR * 60 * 60 * 1000); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Time:") + .right(formatDuration(millis, "H'h' m'm' s's'", true)) + .build()); + } + } + + return super.render(graphics); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceConfig.java new file mode 100644 index 0000000000..c67250355a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, Seth + * 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.blastfurnace; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("blastfurnace") +public interface BlastFurnaceConfig extends Config +{ + @ConfigItem( + keyName = "showConveyorBelt", + name = "Show conveyor belt clickbox", + description = "Configures whether or not the clickbox for the conveyor belt is displayed", + position = 1 + ) + default boolean showConveyorBelt() + { + return false; + } + + @ConfigItem( + keyName = "showBarDispenser", + name = "Show bar dispenser clickbox", + description = "Configures whether or not the clickbox for the bar dispenser is displayed", + position = 2 + ) + default boolean showBarDispenser() + { + return false; + } + + @ConfigItem( + keyName = "showCofferTime", + name = "Show coffer time remaining", + description = "Configures whether or not the coffer time remaining is displayed", + position = 3 + ) + default boolean showCofferTime() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceOverlay.java new file mode 100644 index 0000000000..d24c8b4d4a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceOverlay.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018, Seth + * 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.blastfurnace; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import javax.inject.Inject; +import net.runelite.api.Client; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import net.runelite.client.game.ItemManager; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.ComponentOrientation; +import net.runelite.client.ui.overlay.components.ImageComponent; + +class BlastFurnaceOverlay extends OverlayPanel +{ + private final Client client; + private final BlastFurnacePlugin plugin; + + @Inject + private ItemManager itemManager; + + @Inject + BlastFurnaceOverlay(Client client, BlastFurnacePlugin plugin) + { + super(plugin); + this.plugin = plugin; + this.client = client; + setPosition(OverlayPosition.TOP_LEFT); + panelComponent.setOrientation(ComponentOrientation.HORIZONTAL); + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "Blast furnace overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (plugin.getConveyorBelt() == null) + { + return null; + } + + for (BarsOres varbit : BarsOres.values()) + { + int amount = client.getVar(varbit.getVarbit()); + + if (amount == 0) + { + continue; + } + + panelComponent.getChildren().add(new ImageComponent(getImage(varbit.getItemID(), amount))); + } + + return super.render(graphics); + } + + private BufferedImage getImage(int itemID, int amount) + { + BufferedImage image = itemManager.getImage(itemID, amount, true); + return image; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnacePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnacePlugin.java new file mode 100644 index 0000000000..15d602a324 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnacePlugin.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2018, Seth + * Copyright (c) 2019, Brandon White + * 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.blastfurnace; + +import com.google.inject.Provides; +import java.time.Duration; +import java.time.Instant; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.GameState; +import static net.runelite.api.NullObjectID.NULL_9092; +import static net.runelite.api.ObjectID.CONVEYOR_BELT; +import net.runelite.api.Skill; +import net.runelite.api.events.GameObjectDespawned; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.util.Text; + +@PluginDescriptor( + name = "Blast Furnace", + description = "Show helpful information for the Blast Furnace minigame", + tags = {"minigame", "overlay", "skilling", "smithing"} +) +public class BlastFurnacePlugin extends Plugin +{ + private static final int BAR_DISPENSER = NULL_9092; + private static final String FOREMAN_PERMISSION_TEXT = "Okay, you can use the furnace for ten minutes. Remember, you only need half as much coal as with a regular furnace."; + + @Getter(AccessLevel.PACKAGE) + private GameObject conveyorBelt; + + @Getter(AccessLevel.PACKAGE) + private GameObject barDispenser; + + private ForemanTimer foremanTimer; + + @Inject + private OverlayManager overlayManager; + + @Inject + private BlastFurnaceOverlay overlay; + + @Inject + private BlastFurnaceCofferOverlay cofferOverlay; + + @Inject + private BlastFurnaceClickBoxOverlay clickBoxOverlay; + + @Inject + private Client client; + + @Inject + private ItemManager itemManager; + + @Inject + private InfoBoxManager infoBoxManager; + + @Override + protected void startUp() throws Exception + { + overlayManager.add(overlay); + overlayManager.add(cofferOverlay); + overlayManager.add(clickBoxOverlay); + } + + @Override + protected void shutDown() + { + infoBoxManager.removeIf(ForemanTimer.class::isInstance); + overlayManager.remove(overlay); + overlayManager.remove(cofferOverlay); + overlayManager.remove(clickBoxOverlay); + conveyorBelt = null; + barDispenser = null; + foremanTimer = null; + } + + @Provides + BlastFurnaceConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(BlastFurnaceConfig.class); + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned event) + { + GameObject gameObject = event.getGameObject(); + + switch (gameObject.getId()) + { + case CONVEYOR_BELT: + conveyorBelt = gameObject; + break; + + case BAR_DISPENSER: + barDispenser = gameObject; + break; + } + } + + @Subscribe + public void onGameObjectDespawned(GameObjectDespawned event) + { + GameObject gameObject = event.getGameObject(); + + switch (gameObject.getId()) + { + case CONVEYOR_BELT: + conveyorBelt = null; + break; + + case BAR_DISPENSER: + barDispenser = null; + break; + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOADING) + { + conveyorBelt = null; + barDispenser = null; + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + Widget npcDialog = client.getWidget(WidgetInfo.DIALOG_NPC_TEXT); + if (npcDialog == null) + { + return; + } + + // blocking dialog check until 5 minutes needed to avoid re-adding while dialog message still displayed + boolean shouldCheckForemanFee = client.getRealSkillLevel(Skill.SMITHING) < 60 + && (foremanTimer == null || Duration.between(Instant.now(), foremanTimer.getEndTime()).toMinutes() <= 5); + + if (shouldCheckForemanFee) + { + String npcText = Text.sanitizeMultilineText(npcDialog.getText()); + + if (npcText.equals(FOREMAN_PERMISSION_TEXT)) + { + infoBoxManager.removeIf(ForemanTimer.class::isInstance); + + foremanTimer = new ForemanTimer(this, itemManager); + infoBoxManager.addInfoBox(foremanTimer); + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/ForemanTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/ForemanTimer.java new file mode 100644 index 0000000000..0a246b690c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/ForemanTimer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019, Brandon White + * 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.blastfurnace; + +import java.time.temporal.ChronoUnit; +import net.runelite.api.ItemID; +import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.overlay.infobox.Timer; + +class ForemanTimer extends Timer +{ + private static final String TOOLTIP_TEXT = "Foreman Fee"; + + ForemanTimer(BlastFurnacePlugin plugin, ItemManager itemManager) + { + super(10, ChronoUnit.MINUTES, itemManager.getImage(ItemID.COAL_BAG), plugin); + + setTooltip(TOOLTIP_TEXT); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java new file mode 100644 index 0000000000..ef9336bbee --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018, Unmoon + * 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.blastmine; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.ItemID; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import net.runelite.api.Varbits; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.game.ItemManager; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.ComponentOrientation; +import net.runelite.client.ui.overlay.components.ImageComponent; + +class BlastMineOreCountOverlay extends OverlayPanel +{ + private final Client client; + private final BlastMinePluginConfig config; + private final ItemManager itemManager; + + @Inject + private BlastMineOreCountOverlay(BlastMinePlugin plugin, Client client, BlastMinePluginConfig config, ItemManager itemManager) + { + super(plugin); + setPosition(OverlayPosition.TOP_LEFT); + this.client = client; + this.config = config; + this.itemManager = itemManager; + panelComponent.setOrientation(ComponentOrientation.HORIZONTAL); + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "Blast mine overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + final Widget blastMineWidget = client.getWidget(WidgetInfo.BLAST_MINE); + + if (blastMineWidget == null) + { + return null; + } + + if (config.showOreOverlay()) + { + blastMineWidget.setHidden(true); + panelComponent.getChildren().add(new ImageComponent(getImage(ItemID.COAL, client.getVar(Varbits.BLAST_MINE_COAL)))); + panelComponent.getChildren().add(new ImageComponent(getImage(ItemID.GOLD_ORE, client.getVar(Varbits.BLAST_MINE_GOLD)))); + panelComponent.getChildren().add(new ImageComponent(getImage(ItemID.MITHRIL_ORE, client.getVar(Varbits.BLAST_MINE_MITHRIL)))); + panelComponent.getChildren().add(new ImageComponent(getImage(ItemID.ADAMANTITE_ORE, client.getVar(Varbits.BLAST_MINE_ADAMANTITE)))); + panelComponent.getChildren().add(new ImageComponent(getImage(ItemID.RUNITE_ORE, client.getVar(Varbits.BLAST_MINE_RUNITE)))); + } + else + { + blastMineWidget.setHidden(false); + } + + return super.render(graphics); + } + + private BufferedImage getImage(int itemID, int amount) + { + return itemManager.getImage(itemID, amount, true); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePlugin.java new file mode 100644 index 0000000000..7ddc7e8a80 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePlugin.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2018, Unmoon + * 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.blastmine; + +import com.google.inject.Provides; +import java.util.HashMap; +import java.util.Map; +import javax.inject.Inject; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.GameState; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; + +@PluginDescriptor( + name = "Blast Mine", + description = "Show helpful information for the Blast Mine minigame", + tags = {"explode", "explosive", "mining", "minigame", "skilling"} +) +public class BlastMinePlugin extends Plugin +{ + @Getter + private final Map rocks = new HashMap<>(); + + @Inject + private OverlayManager overlayManager; + + @Inject + private Client client; + + @Inject + private BlastMineRockOverlay blastMineRockOverlay; + + @Inject + private BlastMineOreCountOverlay blastMineOreCountOverlay; + + @Provides + BlastMinePluginConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(BlastMinePluginConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(blastMineRockOverlay); + overlayManager.add(blastMineOreCountOverlay); + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(blastMineRockOverlay); + overlayManager.remove(blastMineOreCountOverlay); + final Widget blastMineWidget = client.getWidget(WidgetInfo.BLAST_MINE); + + if (blastMineWidget != null) + { + blastMineWidget.setHidden(false); + } + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned event) + { + final GameObject gameObject = event.getGameObject(); + BlastMineRockType blastMineRockType = BlastMineRockType.getRockType(gameObject.getId()); + if (blastMineRockType == null) + { + return; + } + + final BlastMineRock newRock = new BlastMineRock(gameObject, blastMineRockType); + final BlastMineRock oldRock = rocks.get(gameObject.getWorldLocation()); + + if (oldRock == null || oldRock.getType() != newRock.getType()) + { + rocks.put(gameObject.getWorldLocation(), newRock); + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOADING) + { + rocks.clear(); + } + } + + @Subscribe + public void onGameTick(GameTick gameTick) + { + if (rocks.isEmpty()) + { + return; + } + + rocks.values().removeIf(rock -> + (rock.getRemainingTimeRelative() == 1 && rock.getType() != BlastMineRockType.NORMAL) || + (rock.getRemainingFuseTimeRelative() == 1 && rock.getType() == BlastMineRockType.LIT)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePluginConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePluginConfig.java new file mode 100644 index 0000000000..76848a66e2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMinePluginConfig.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2018, Unmoon + * 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.blastmine; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +import java.awt.Color; + +@ConfigGroup("blastmine") +public interface BlastMinePluginConfig extends Config +{ + @ConfigItem( + position = 0, + keyName = "showOreOverlay", + name = "Show ore overlay", + description = "Configures whether or not the ore count overlay is displayed" + ) + default boolean showOreOverlay() + { + return true; + } + + @ConfigItem( + position = 1, + keyName = "showRockIconOverlay", + name = "Show icons overlay", + description = "Configures whether or not the icon overlay is displayed" + ) + default boolean showRockIconOverlay() + { + return true; + } + + @ConfigItem( + position = 2, + keyName = "showTimerOverlay", + name = "Show timer overlay", + description = "Configures whether or not the timer overlay is displayed" + ) + default boolean showTimerOverlay() + { + return true; + } + + @ConfigItem( + position = 3, + keyName = "showWarningOverlay", + name = "Show explosion warning", + description = "Configures whether or not the explosion warning overlay is displayed" + ) + default boolean showWarningOverlay() + { + return true; + } + + @ConfigItem( + position = 4, + keyName = "hexTimerColor", + name = "Timer color", + description = "Color of timer overlay" + ) + default Color getTimerColor() + { + return new Color(217, 54, 0); + } + + @ConfigItem( + position = 5, + keyName = "hexWarningColor", + name = "Warning color", + description = "Color of warning overlay" + ) + default Color getWarningColor() + { + return new Color(217, 54, 0); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRock.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRock.java new file mode 100644 index 0000000000..1f1cec4fb5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRock.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, Unmoon + * 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.blastmine; + +import java.time.Duration; +import java.time.Instant; +import lombok.Getter; +import net.runelite.api.GameObject; + +class BlastMineRock +{ + private static final Duration PLANT_TIME = Duration.ofSeconds(30); + private static final Duration FUSE_TIME = Duration.ofMillis(4200); + + @Getter + private final GameObject gameObject; + + @Getter + private final BlastMineRockType type; + + private final Instant creationTime = Instant.now(); + + BlastMineRock(final GameObject gameObject, BlastMineRockType blastMineRockType) + { + this.gameObject = gameObject; + this.type = blastMineRockType; + } + + double getRemainingFuseTimeRelative() + { + Duration duration = Duration.between(creationTime, Instant.now()); + return duration.compareTo(FUSE_TIME) < 0 ? (double) duration.toMillis() / FUSE_TIME.toMillis() : 1; + } + + double getRemainingTimeRelative() + { + Duration duration = Duration.between(creationTime, Instant.now()); + return duration.compareTo(PLANT_TIME) < 0 ? (double) duration.toMillis() / PLANT_TIME.toMillis() : 1; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockOverlay.java new file mode 100644 index 0000000000..92fbdfe221 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockOverlay.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2018, Unmoon + * 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.blastmine; + +import com.google.common.collect.ImmutableSet; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.image.BufferedImage; +import java.util.Map; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.ItemID; +import net.runelite.api.NullObjectID; +import net.runelite.api.ObjectID; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.Tile; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.widgets.Widget; +import net.runelite.client.game.ItemManager; +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.ProgressPieComponent; + +public class BlastMineRockOverlay extends Overlay +{ + private static final int MAX_DISTANCE = 16; + private static final int WARNING_DISTANCE = 2; + private static final ImmutableSet WALL_OBJECTS = ImmutableSet.of( + NullObjectID.NULL_28570, NullObjectID.NULL_28571, NullObjectID.NULL_28572, NullObjectID.NULL_28573, NullObjectID.NULL_28574, + NullObjectID.NULL_28575, NullObjectID.NULL_28576, NullObjectID.NULL_28577, NullObjectID.NULL_28578, + ObjectID.HARD_ROCK, ObjectID.HARD_ROCK_28580, ObjectID.CAVITY, ObjectID.CAVITY_28582, + ObjectID.POT_OF_DYNAMITE, ObjectID.POT_OF_DYNAMITE_28584, ObjectID.POT_OF_DYNAMITE_28585, ObjectID.POT_OF_DYNAMITE_28586, + ObjectID.SHATTERED_ROCKFACE, ObjectID.SHATTERED_ROCKFACE_28588); + + private final Client client; + private final BlastMinePlugin plugin; + private final BlastMinePluginConfig config; + + private final BufferedImage chiselIcon; + private final BufferedImage dynamiteIcon; + private final BufferedImage tinderboxIcon; + + @Inject + private BlastMineRockOverlay(Client client, BlastMinePlugin plugin, BlastMinePluginConfig config, ItemManager itemManager) + { + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + this.client = client; + this.plugin = plugin; + this.config = config; + chiselIcon = itemManager.getImage(ItemID.CHISEL); + dynamiteIcon = itemManager.getImage(ItemID.DYNAMITE); + tinderboxIcon = itemManager.getImage(ItemID.TINDERBOX); + } + + @Override + public Dimension render(Graphics2D graphics) + { + Map rocks = plugin.getRocks(); + if (rocks.isEmpty()) + { + return null; + } + + final Tile[][][] tiles = client.getScene().getTiles(); + final Widget viewport = client.getViewportWidget(); + + for (final BlastMineRock rock : rocks.values()) + { + if (viewport == null || + rock.getGameObject().getCanvasLocation() == null || + rock.getGameObject().getWorldLocation().distanceTo(client.getLocalPlayer().getWorldLocation()) > MAX_DISTANCE) + { + continue; + } + + switch (rock.getType()) + { + case NORMAL: + drawIconOnRock(graphics, rock, chiselIcon); + break; + case CHISELED: + drawIconOnRock(graphics, rock, dynamiteIcon); + break; + case LOADED: + drawIconOnRock(graphics, rock, tinderboxIcon); + break; + case LIT: + drawTimerOnRock(graphics, rock, config.getTimerColor()); + drawAreaWarning(graphics, rock, config.getWarningColor(), tiles); + break; + } + } + + return null; + } + + private void drawIconOnRock(Graphics2D graphics, BlastMineRock rock, BufferedImage icon) + { + if (!config.showRockIconOverlay()) + { + return; + } + + Point loc = Perspective.getCanvasImageLocation(client, rock.getGameObject().getLocalLocation(), icon, 150); + + if (loc != null) + { + graphics.drawImage(icon, loc.getX(), loc.getY(), null); + } + } + + private void drawTimerOnRock(Graphics2D graphics, BlastMineRock rock, Color color) + { + if (!config.showTimerOverlay()) + { + return; + } + + Point loc = Perspective.localToCanvas(client, rock.getGameObject().getLocalLocation(), rock.getGameObject().getPlane(), 150); + + if (loc != null) + { + final double timeLeft = 1 - rock.getRemainingFuseTimeRelative(); + final ProgressPieComponent pie = new ProgressPieComponent(); + pie.setFill(color); + pie.setBorderColor(color); + pie.setPosition(loc); + pie.setProgress(timeLeft); + pie.render(graphics); + } + } + + private void drawAreaWarning(Graphics2D graphics, BlastMineRock rock, Color color, Tile[][][] tiles) + { + if (!config.showWarningOverlay()) + { + return; + } + + final int z = client.getPlane(); + int x = rock.getGameObject().getLocalLocation().getX() / Perspective.LOCAL_TILE_SIZE; + int y = rock.getGameObject().getLocalLocation().getY() / Perspective.LOCAL_TILE_SIZE; + final int orientation = tiles[z][x][y].getWallObject().getOrientationA(); + + switch (orientation) //calculate explosion around the tile in front of the wall + { + case 1: + x--; + break; + case 4: + x++; + break; + case 8: + y--; + break; + default: + y++; + } + + for (int i = -WARNING_DISTANCE; i <= WARNING_DISTANCE; i++) + { + for (int j = -WARNING_DISTANCE; j <= WARNING_DISTANCE; j++) + { + final GameObject gameObject = tiles[z][x + i][y + j].getGameObjects()[0]; + + //check if tile is empty, or is a wall... + if (gameObject == null || !WALL_OBJECTS.contains(gameObject.getId())) + { + final LocalPoint localTile = new LocalPoint( + (x + i) * Perspective.LOCAL_TILE_SIZE + Perspective.LOCAL_TILE_SIZE / 2, + (y + j) * Perspective.LOCAL_TILE_SIZE + Perspective.LOCAL_TILE_SIZE / 2); + final Polygon poly = Perspective.getCanvasTilePoly(client, localTile); + + if (poly != null) + { + graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 100)); + graphics.fillPolygon(poly); + } + } + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockType.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockType.java new file mode 100644 index 0000000000..20e1d37f58 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineRockType.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018, Unmoon + * 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.blastmine; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import lombok.Getter; +import net.runelite.api.ObjectID; + +public enum BlastMineRockType +{ + NORMAL(ObjectID.HARD_ROCK, ObjectID.HARD_ROCK_28580), + CHISELED(ObjectID.CAVITY, ObjectID.CAVITY_28582), + LOADED(ObjectID.POT_OF_DYNAMITE, ObjectID.POT_OF_DYNAMITE_28584), + LIT(ObjectID.POT_OF_DYNAMITE_28585, ObjectID.POT_OF_DYNAMITE_28586), + EXPLODED(ObjectID.SHATTERED_ROCKFACE, ObjectID.SHATTERED_ROCKFACE_28588); + + private static final Map rockTypes; + + static + { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + + for (BlastMineRockType type : values()) + { + for (int spotId : type.getObjectIds()) + { + builder.put(spotId, type); + } + } + + rockTypes = builder.build(); + } + + @Getter + private final int[] objectIds; + + BlastMineRockType(int... objectIds) + { + this.objectIds = objectIds; + } + + public static BlastMineRockType getRockType(int objectId) + { + return rockTypes.get(objectId); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java new file mode 100644 index 0000000000..001e8ae5c8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018 Kamiel + * 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.boosts; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.client.ui.overlay.infobox.InfoBox; +import net.runelite.client.ui.overlay.infobox.InfoBoxPriority; + +public class BoostIndicator extends InfoBox +{ + private final BoostsPlugin plugin; + private final BoostsConfig config; + private final Client client; + + @Getter + private final Skill skill; + + BoostIndicator(Skill skill, BufferedImage image, BoostsPlugin plugin, Client client, BoostsConfig config) + { + super(image, plugin); + this.plugin = plugin; + this.config = config; + this.client = client; + this.skill = skill; + setTooltip(skill.getName() + " boost"); + setPriority(InfoBoxPriority.HIGH); + } + + @Override + public String getText() + { + if (!config.useRelativeBoost()) + { + return String.valueOf(client.getBoostedSkillLevel(skill)); + } + + int boost = client.getBoostedSkillLevel(skill) - client.getRealSkillLevel(skill); + String text = String.valueOf(boost); + if (boost > 0) + { + text = "+" + text; + } + + return text; + } + + @Override + public Color getTextColor() + { + int boosted = client.getBoostedSkillLevel(skill), + base = client.getRealSkillLevel(skill); + + if (boosted < base) + { + return new Color(238, 51, 51); + } + + return boosted - base <= config.boostThreshold() ? Color.YELLOW : Color.GREEN; + } + + @Override + public boolean render() + { + return config.displayInfoboxes() && plugin.canShowBoosts() && plugin.getSkillsToDisplay().contains(getSkill()); + } + + @Override + public String getName() + { + return "Boost " + skill.getName(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java new file mode 100644 index 0000000000..dbb0d6d731 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2017, Seth + * 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.boosts; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("boosts") +public interface BoostsConfig extends Config +{ + enum DisplayChangeMode + { + ALWAYS, + BOOSTED, + NEVER + } + + enum DisplayBoosts + { + NONE, + COMBAT, + NON_COMBAT, + BOTH + } + + @ConfigItem( + keyName = "displayBoosts", + name = "Display Boosts", + description = "Configures which skill boosts to display", + position = 1 + ) + default DisplayBoosts displayBoosts() + { + return DisplayBoosts.BOTH; + } + + @ConfigItem( + keyName = "relativeBoost", + name = "Use Relative Boosts", + description = "Configures whether or not relative boost is used", + position = 2 + ) + default boolean useRelativeBoost() + { + return false; + } + + @ConfigItem( + keyName = "displayIndicators", + name = "Display as infoboxes", + description = "Configures whether or not to display the boost as infoboxes", + position = 3 + ) + default boolean displayInfoboxes() + { + return false; + } + + @ConfigItem( + keyName = "displayNextBuffChange", + name = "Display next buff change", + description = "Configures whether or not to display when the next buffed stat change will be", + position = 4 + ) + default DisplayChangeMode displayNextBuffChange() + { + return DisplayChangeMode.BOOSTED; + } + + @ConfigItem( + keyName = "displayNextDebuffChange", + name = "Display next debuff change", + description = "Configures whether or not to display when the next debuffed stat change will be", + position = 5 + ) + default DisplayChangeMode displayNextDebuffChange() + { + return DisplayChangeMode.NEVER; + } + + @ConfigItem( + keyName = "boostThreshold", + name = "Boost amount threshold", + description = "The threshold at which boosted levels will be displayed in a different color. A value of 0 will disable the feature.", + position = 6 + ) + default int boostThreshold() + { + return 0; + } + + @ConfigItem( + keyName = "notifyOnBoost", + name = "Notify on boost threshold", + description = "Configures whether or not a notification will be sent for boosted stats.", + position = 7 + ) + default boolean notifyOnBoost() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java new file mode 100644 index 0000000000..fd53503444 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-2017, 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.boosts; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.util.Set; +import javax.inject.Inject; +import net.runelite.api.Client; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import net.runelite.api.Skill; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.util.ColorUtil; + +class BoostsOverlay extends OverlayPanel +{ + private final Client client; + private final BoostsConfig config; + private final BoostsPlugin plugin; + + @Inject + private BoostsOverlay(Client client, BoostsConfig config, BoostsPlugin plugin) + { + super(plugin); + this.plugin = plugin; + this.client = client; + this.config = config; + setPosition(OverlayPosition.TOP_LEFT); + setPriority(OverlayPriority.MED); + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "Boosts overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (config.displayInfoboxes()) + { + return null; + } + + int nextChange = plugin.getChangeDownTicks(); + + if (nextChange != -1) + { + panelComponent.getChildren().add(LineComponent.builder() + .left("Next + restore in") + .right(String.valueOf(plugin.getChangeTime(nextChange))) + .build()); + } + + nextChange = plugin.getChangeUpTicks(); + + if (nextChange != -1) + { + panelComponent.getChildren().add(LineComponent.builder() + .left("Next - restore in") + .right(String.valueOf(plugin.getChangeTime(nextChange))) + .build()); + } + + final Set boostedSkills = plugin.getSkillsToDisplay(); + + if (boostedSkills.isEmpty()) + { + return super.render(graphics); + } + + if (plugin.canShowBoosts()) + { + for (Skill skill : boostedSkills) + { + final int boosted = client.getBoostedSkillLevel(skill); + final int base = client.getRealSkillLevel(skill); + final int boost = boosted - base; + final Color strColor = getTextColor(boost); + String str; + + if (config.useRelativeBoost()) + { + str = String.valueOf(boost); + if (boost > 0) + { + str = "+" + str; + } + } + else + { + str = ColorUtil.prependColorTag(Integer.toString(boosted), strColor) + + ColorUtil.prependColorTag("/" + base, Color.WHITE); + } + + panelComponent.getChildren().add(LineComponent.builder() + .left(skill.getName()) + .right(str) + .rightColor(strColor) + .build()); + } + } + + return super.render(graphics); + } + + private Color getTextColor(int boost) + { + if (boost < 0) + { + return new Color(238, 51, 51); + } + + return boost <= config.boostThreshold() ? Color.YELLOW : Color.GREEN; + + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java new file mode 100644 index 0000000000..c9fd49c97b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2016-2017, 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.boosts; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Provides; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.Constants; +import net.runelite.api.Prayer; +import net.runelite.api.Skill; +import net.runelite.client.events.ConfigChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.StatChanged; +import net.runelite.client.Notifier; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.game.SkillIconManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.util.ImageUtil; + +@PluginDescriptor( + name = "Boosts Information", + description = "Show combat and/or skill boost information", + tags = {"combat", "notifications", "skilling", "overlay"} +) +@Singleton +public class BoostsPlugin extends Plugin +{ + private static final Set BOOSTABLE_COMBAT_SKILLS = ImmutableSet.of( + Skill.ATTACK, + Skill.STRENGTH, + Skill.DEFENCE, + Skill.RANGED, + Skill.MAGIC); + + private static final Set BOOSTABLE_NON_COMBAT_SKILLS = ImmutableSet.of( + Skill.MINING, Skill.AGILITY, Skill.SMITHING, Skill.HERBLORE, Skill.FISHING, Skill.THIEVING, + Skill.COOKING, Skill.CRAFTING, Skill.FIREMAKING, Skill.FLETCHING, Skill.WOODCUTTING, Skill.RUNECRAFT, + Skill.SLAYER, Skill.FARMING, Skill.CONSTRUCTION, Skill.HUNTER); + + @Inject + private Notifier notifier; + + @Inject + private Client client; + + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private OverlayManager overlayManager; + + @Inject + private BoostsOverlay boostsOverlay; + + @Inject + private BoostsConfig config; + + @Inject + private SkillIconManager skillIconManager; + + @Getter + private final Set skillsToDisplay = EnumSet.noneOf(Skill.class); + + private final Set shownSkills = EnumSet.noneOf(Skill.class); + + private boolean isChangedDown = false; + private boolean isChangedUp = false; + private final int[] lastSkillLevels = new int[Skill.values().length - 1]; + private int lastChangeDown = -1; + private int lastChangeUp = -1; + private boolean preserveBeenActive = false; + private long lastTickMillis; + + @Provides + BoostsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(BoostsConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(boostsOverlay); + + updateShownSkills(); + Arrays.fill(lastSkillLevels, -1); + + // Add infoboxes for everything at startup and then determine inside if it will be rendered + infoBoxManager.addInfoBox(new StatChangeIndicator(true, ImageUtil.getResourceStreamFromClass(getClass(), "debuffed.png"), this, config)); + infoBoxManager.addInfoBox(new StatChangeIndicator(false, ImageUtil.getResourceStreamFromClass(getClass(), "buffed.png"), this, config)); + + for (final Skill skill : Skill.values()) + { + if (skill != Skill.OVERALL) + { + infoBoxManager.addInfoBox(new BoostIndicator(skill, skillIconManager.getSkillImage(skill), this, client, config)); + } + } + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(boostsOverlay); + infoBoxManager.removeIf(t -> t instanceof BoostIndicator || t instanceof StatChangeIndicator); + preserveBeenActive = false; + lastChangeDown = -1; + lastChangeUp = -1; + isChangedUp = false; + isChangedDown = false; + skillsToDisplay.clear(); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case LOGIN_SCREEN: + case HOPPING: + // After world hop and log out timers are in undefined state so just reset + lastChangeDown = -1; + lastChangeUp = -1; + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!event.getGroup().equals("boosts")) + { + return; + } + + updateShownSkills(); + + if (config.displayNextBuffChange() == BoostsConfig.DisplayChangeMode.NEVER) + { + lastChangeDown = -1; + } + + if (config.displayNextDebuffChange() == BoostsConfig.DisplayChangeMode.NEVER) + { + lastChangeUp = -1; + } + } + + @Subscribe + public void onStatChanged(StatChanged statChanged) + { + Skill skill = statChanged.getSkill(); + + if (!BOOSTABLE_COMBAT_SKILLS.contains(skill) && !BOOSTABLE_NON_COMBAT_SKILLS.contains(skill)) + { + return; + } + + int skillIdx = skill.ordinal(); + int last = lastSkillLevels[skillIdx]; + int cur = client.getBoostedSkillLevel(skill); + + if (cur == last - 1) + { + // Stat was restored down (from buff) + lastChangeDown = client.getTickCount(); + } + + if (cur == last + 1) + { + // Stat was restored up (from debuff) + lastChangeUp = client.getTickCount(); + } + + lastSkillLevels[skillIdx] = cur; + updateBoostedStats(); + + int boostThreshold = config.boostThreshold(); + + if (boostThreshold != 0 && config.notifyOnBoost()) + { + int real = client.getRealSkillLevel(skill); + int lastBoost = last - real; + int boost = cur - real; + if (boost <= boostThreshold && boostThreshold < lastBoost) + { + notifier.notify(skill.getName() + " level is getting low!"); + } + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + lastTickMillis = System.currentTimeMillis(); + + if (getChangeUpTicks() <= 0) + { + switch (config.displayNextDebuffChange()) + { + case ALWAYS: + if (lastChangeUp != -1) + { + lastChangeUp = client.getTickCount(); + } + + break; + case BOOSTED: + case NEVER: + lastChangeUp = -1; + break; + } + } + + if (getChangeDownTicks() <= 0) + { + switch (config.displayNextBuffChange()) + { + case ALWAYS: + if (lastChangeDown != -1) + { + lastChangeDown = client.getTickCount(); + } + + break; + case BOOSTED: + case NEVER: + lastChangeDown = -1; + break; + } + } + } + + private void updateShownSkills() + { + switch (config.displayBoosts()) + { + case NONE: + shownSkills.removeAll(BOOSTABLE_COMBAT_SKILLS); + shownSkills.removeAll(BOOSTABLE_NON_COMBAT_SKILLS); + break; + case COMBAT: + shownSkills.addAll(BOOSTABLE_COMBAT_SKILLS); + shownSkills.removeAll(BOOSTABLE_NON_COMBAT_SKILLS); + break; + case NON_COMBAT: + shownSkills.removeAll(BOOSTABLE_COMBAT_SKILLS); + shownSkills.addAll(BOOSTABLE_NON_COMBAT_SKILLS); + break; + case BOTH: + shownSkills.addAll(BOOSTABLE_COMBAT_SKILLS); + shownSkills.addAll(BOOSTABLE_NON_COMBAT_SKILLS); + break; + } + updateBoostedStats(); + } + + private void updateBoostedStats() + { + // Reset is boosted + isChangedDown = false; + isChangedUp = false; + skillsToDisplay.clear(); + + // Check if we are still boosted + for (final Skill skill : Skill.values()) + { + if (!shownSkills.contains(skill)) + { + continue; + } + + final int boosted = client.getBoostedSkillLevel(skill); + final int base = client.getRealSkillLevel(skill); + + if (boosted > base) + { + isChangedUp = true; + } + else if (boosted < base) + { + isChangedDown = true; + } + + if (boosted != base) + { + skillsToDisplay.add(skill); + } + } + } + + boolean canShowBoosts() + { + return isChangedDown || isChangedUp; + } + + /** + * Calculates the amount of time until boosted stats decay, + * accounting for the effect of preserve prayer. + * Preserve extends the time of boosted stats by 50% while active. + * The length of a boost is split into 4 sections of 15 seconds each. + * If the preserve prayer is active for the entire duration of the final + * section it will "activate" adding an additional 15 second section + * to the boost timing. If again the preserve prayer is active for that + * entire section a second 15 second section will be added. + * + * Preserve is only required to be on for the 4th and 5th sections of the boost timer + * to gain full effect (seconds 45-75). + * + * @return integer value in ticks until next boost change + */ + int getChangeDownTicks() + { + if (lastChangeDown == -1 || + config.displayNextBuffChange() == BoostsConfig.DisplayChangeMode.NEVER || + (config.displayNextBuffChange() == BoostsConfig.DisplayChangeMode.BOOSTED && !isChangedUp)) + { + return -1; + } + + int ticksSinceChange = client.getTickCount() - lastChangeDown; + boolean isPreserveActive = client.isPrayerActive(Prayer.PRESERVE); + + if ((isPreserveActive && (ticksSinceChange < 75 || preserveBeenActive)) || ticksSinceChange > 125) + { + preserveBeenActive = true; + return 150 - ticksSinceChange; + } + + preserveBeenActive = false; + return (ticksSinceChange > 100) ? 125 - ticksSinceChange : 100 - ticksSinceChange; + } + + /** + * Restoration from debuff is separate timer as restoration from buff because of preserve messing up the buff timer. + * Restoration timer is always in 100 tick cycles. + * + * @return integer value in ticks until next stat restoration up + */ + int getChangeUpTicks() + { + if (lastChangeUp == -1 || + config.displayNextDebuffChange() == BoostsConfig.DisplayChangeMode.NEVER || + (config.displayNextDebuffChange() == BoostsConfig.DisplayChangeMode.BOOSTED && !isChangedDown)) + { + return -1; + } + + int ticksSinceChange = client.getTickCount() - lastChangeUp; + return 100 - ticksSinceChange; + } + + + /** + * Converts tick-based time to accurate second time + * @param time tick-based time + * @return second-based time + */ + int getChangeTime(final int time) + { + final long diff = System.currentTimeMillis() - lastTickMillis; + return time != -1 ? (int)((time * Constants.GAME_TICK_LENGTH - diff) / 1000d) : time; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java new file mode 100644 index 0000000000..5c9ef73762 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, Seth + * 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.boosts; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import net.runelite.client.ui.overlay.infobox.InfoBox; +import net.runelite.client.ui.overlay.infobox.InfoBoxPriority; + +public class StatChangeIndicator extends InfoBox +{ + private final boolean up; + private final BoostsPlugin plugin; + private final BoostsConfig config; + + StatChangeIndicator(boolean up, BufferedImage image, BoostsPlugin plugin, BoostsConfig config) + { + super(image, plugin); + this.up = up; + this.plugin = plugin; + this.config = config; + setPriority(InfoBoxPriority.MED); + setTooltip(up ? "Next debuff change" : "Next buff change"); + } + + @Override + public String getText() + { + return String.format("%02d", plugin.getChangeTime(up ? plugin.getChangeUpTicks() : plugin.getChangeDownTicks())); + } + + @Override + public Color getTextColor() + { + return (up ? plugin.getChangeUpTicks() : plugin.getChangeDownTicks()) < 10 ? Color.RED.brighter() : Color.WHITE; + } + + @Override + public boolean render() + { + final int time = up ? plugin.getChangeUpTicks() : plugin.getChangeDownTicks(); + return config.displayInfoboxes() && time != -1; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java new file mode 100644 index 0000000000..078463bfb3 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-2017, Cameron Moberg + * Copyright (c) 2017, 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.bosstimer; + +import com.google.common.collect.ImmutableMap; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import net.runelite.api.ItemID; +import net.runelite.api.NpcID; + +enum Boss +{ + GENERAL_GRAARDOR(NpcID.GENERAL_GRAARDOR, 90, ChronoUnit.SECONDS, ItemID.PET_GENERAL_GRAARDOR), + KRIL_TSUTSAROTH(NpcID.KRIL_TSUTSAROTH, 90, ChronoUnit.SECONDS, ItemID.PET_KRIL_TSUTSAROTH), + KREEARRA(NpcID.KREEARRA, 90, ChronoUnit.SECONDS, ItemID.PET_KREEARRA), + COMMANDER_ZILYANA(NpcID.COMMANDER_ZILYANA, 90, ChronoUnit.SECONDS, ItemID.PET_ZILYANA), + CALLISTO(NpcID.CALLISTO_6609, 30, ChronoUnit.SECONDS, ItemID.CALLISTO_CUB), + CHAOS_ELEMENTAL(NpcID.CHAOS_ELEMENTAL, 60, ChronoUnit.SECONDS, ItemID.PET_CHAOS_ELEMENTAL), + CHAOS_FANATIC(NpcID.CHAOS_FANATIC, 30, ChronoUnit.SECONDS, ItemID.ANCIENT_STAFF), + CRAZY_ARCHAEOLOGIST(NpcID.CRAZY_ARCHAEOLOGIST, 30, ChronoUnit.SECONDS, ItemID.FEDORA), + KING_BLACK_DRAGON(NpcID.KING_BLACK_DRAGON, 9, ChronoUnit.SECONDS, ItemID.PRINCE_BLACK_DRAGON), + SCORPIA(NpcID.SCORPIA, 10, ChronoUnit.SECONDS, ItemID.SCORPIAS_OFFSPRING), + VENENATIS(NpcID.VENENATIS_6610, 30, ChronoUnit.SECONDS, ItemID.VENENATIS_SPIDERLING), + VETION(NpcID.VETION_REBORN, 30, ChronoUnit.SECONDS, ItemID.VETION_JR), + DAGANNOTH_PRIME(NpcID.DAGANNOTH_PRIME, 90, ChronoUnit.SECONDS, ItemID.PET_DAGANNOTH_PRIME), + DAGANNOTH_REX(NpcID.DAGANNOTH_REX, 90, ChronoUnit.SECONDS, ItemID.PET_DAGANNOTH_REX), + DAGANNOTH_SUPREME(NpcID.DAGANNOTH_SUPREME, 90, ChronoUnit.SECONDS, ItemID.PET_DAGANNOTH_SUPREME), + CORPOREAL_BEAST(NpcID.CORPOREAL_BEAST, 30, ChronoUnit.SECONDS, ItemID.PET_DARK_CORE), + GIANT_MOLE(NpcID.GIANT_MOLE, 9000, ChronoUnit.MILLIS, ItemID.BABY_MOLE), + DERANGED_ARCHAEOLOGIST(NpcID.DERANGED_ARCHAEOLOGIST, 29400, ChronoUnit.MILLIS, ItemID.UNIDENTIFIED_LARGE_FOSSIL), + CERBERUS(NpcID.CERBERUS, 8400, ChronoUnit.MILLIS, ItemID.HELLPUPPY), + THERMONUCLEAR_SMOKE_DEVIL(NpcID.THERMONUCLEAR_SMOKE_DEVIL, 8400, ChronoUnit.MILLIS, ItemID.PET_SMOKE_DEVIL), + KRAKEN(NpcID.KRAKEN, 8400, ChronoUnit.MILLIS, ItemID.PET_KRAKEN), + KALPHITE_QUEEN(NpcID.KALPHITE_QUEEN_965, 30, ChronoUnit.SECONDS, ItemID.KALPHITE_PRINCESS), + DUSK(NpcID.DUSK_7889, 2, ChronoUnit.MINUTES, ItemID.NOON), + ALCHEMICAL_HYDRA(NpcID.ALCHEMICAL_HYDRA_8622, 25200, ChronoUnit.MILLIS, ItemID.IKKLE_HYDRA), + SARACHNIS(NpcID.SARACHNIS, 10, ChronoUnit.SECONDS, ItemID.SRARACHA), + ZALCANO(NpcID.ZALCANO_9050, 21600, ChronoUnit.MILLIS, ItemID.SMOLCANO); + + private static final Map bosses; + + private final int id; + private final Duration spawnTime; + private final int itemSpriteId; + + static + { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + + for (Boss boss : values()) + { + builder.put(boss.getId(), boss); + } + + bosses = builder.build(); + } + + Boss(int id, long period, ChronoUnit unit, int itemSpriteId) + { + this.id = id; + this.spawnTime = Duration.of(period, unit); + this.itemSpriteId = itemSpriteId; + } + + public int getId() + { + return id; + } + + public Duration getSpawnTime() + { + return spawnTime; + } + + public int getItemSpriteId() + { + return itemSpriteId; + } + + public static Boss find(int id) + { + return bosses.get(id); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java new file mode 100644 index 0000000000..0e6b98c907 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-2017, Cameron Moberg + * Copyright (c) 2017, 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.bosstimer; + +import javax.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.NPC; +import net.runelite.api.events.NpcDespawned; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; + +@PluginDescriptor( + name = "Boss Timers", + description = "Show boss spawn timer overlays", + tags = {"combat", "pve", "overlay", "spawn"} +) +@Slf4j +public class BossTimersPlugin extends Plugin +{ + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private ItemManager itemManager; + + @Override + protected void shutDown() throws Exception + { + infoBoxManager.removeIf(t -> t instanceof RespawnTimer); + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + NPC npc = npcDespawned.getNpc(); + + if (!npc.isDead()) + { + return; + } + + int npcId = npc.getId(); + + Boss boss = Boss.find(npcId); + + if (boss == null) + { + return; + } + + // remove existing timer + infoBoxManager.removeIf(t -> t instanceof RespawnTimer && ((RespawnTimer) t).getBoss() == boss); + + log.debug("Creating spawn timer for {} ({} seconds)", npc.getName(), boss.getSpawnTime()); + + RespawnTimer timer = new RespawnTimer(boss, itemManager.getImage(boss.getItemSpriteId()), this); + timer.setTooltip(npc.getName()); + infoBoxManager.addInfoBox(timer); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/RespawnTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/RespawnTimer.java new file mode 100644 index 0000000000..1d9d077f25 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/RespawnTimer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017, 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.bosstimer; + +import java.awt.image.BufferedImage; +import java.time.temporal.ChronoUnit; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.infobox.Timer; + +class RespawnTimer extends Timer +{ + private final Boss boss; + + public RespawnTimer(Boss boss, BufferedImage bossImage, Plugin plugin) + { + super(boss.getSpawnTime().toMillis(), ChronoUnit.MILLIS, bossImage, plugin); + this.boss = boss; + } + + public Boss getBoss() + { + return boss; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonConfig.java new file mode 100644 index 0000000000..05ba93da2e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonConfig.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-2018, Seth + * 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.cannon; + +import java.awt.Color; +import net.runelite.client.config.Alpha; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Range; +import static net.runelite.client.plugins.cannon.CannonPlugin.MAX_CBALLS; + +@ConfigGroup("cannon") +public interface CannonConfig extends Config +{ + @ConfigItem( + keyName = "showEmptyCannonNotification", + name = "Enable cannon notifications", + description = "Configures whether to notify you when your cannon is low on cannonballs", + position = 1 + ) + default boolean showCannonNotifications() + { + return true; + } + + @Range( + max = MAX_CBALLS + ) + @ConfigItem( + keyName = "lowWarningThreshold", + name = "Low Warning Threshold", + description = "Configures the number of cannonballs remaining before a notification is sent.
Regardless of this value, a notification will still be sent when your cannon is empty.", + position = 2 + ) + default int lowWarningThreshold() + { + return 0; + } + + @ConfigItem( + keyName = "showInfobox", + name = "Show Cannonball infobox", + description = "Configures whether to show the cannonballs in an infobox", + position = 3 + ) + default boolean showInfobox() + { + return false; + } + + @ConfigItem( + keyName = "showDoubleHitSpot", + name = "Show double hit spots", + description = "Configures whether to show the NPC double hit spot", + position = 4 + ) + default boolean showDoubleHitSpot() + { + return false; + } + + @Alpha + @ConfigItem( + keyName = "highlightDoubleHitColor", + name = "Color of double hit spots", + description = "Configures the highlight color of double hit spots", + position = 5 + ) + default Color highlightDoubleHitColor() + { + return Color.RED; + } + + @ConfigItem( + keyName = "showCannonSpots", + name = "Show common cannon spots", + description = "Configures whether to show common cannon spots or not", + position = 6 + ) + default boolean showCannonSpots() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonCounter.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonCounter.java new file mode 100644 index 0000000000..a4b8028e47 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonCounter.java @@ -0,0 +1,52 @@ +/* + * 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.cannon; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import net.runelite.client.ui.overlay.infobox.InfoBox; + +class CannonCounter extends InfoBox +{ + private final CannonPlugin plugin; + + CannonCounter(BufferedImage img, CannonPlugin plugin) + { + super(img, plugin); + this.plugin = plugin; + } + + @Override + public String getText() + { + return String.valueOf(plugin.getCballsLeft()); + } + + @Override + public Color getTextColor() + { + return plugin.getStateColor(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonOverlay.java new file mode 100644 index 0000000000..69e553da75 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonOverlay.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-2018, Seth + * 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.cannon; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Polygon; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; +import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.OverlayUtil; +import net.runelite.client.ui.overlay.components.TextComponent; + +class CannonOverlay extends Overlay +{ + private static final int MAX_DISTANCE = 2500; + + private final Client client; + private final CannonConfig config; + private final CannonPlugin plugin; + private final TextComponent textComponent = new TextComponent(); + + @Inject + CannonOverlay(Client client, CannonConfig config, CannonPlugin plugin) + { + setPosition(OverlayPosition.DYNAMIC); + setPriority(OverlayPriority.MED); + this.client = client; + this.config = config; + this.plugin = plugin; + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!plugin.isCannonPlaced() || plugin.getCannonPosition() == null || plugin.getCannonWorld() != client.getWorld()) + { + return null; + } + + LocalPoint cannonPoint = LocalPoint.fromWorld(client, plugin.getCannonPosition()); + + if (cannonPoint == null) + { + return null; + } + + LocalPoint localLocation = client.getLocalPlayer().getLocalLocation(); + + if (localLocation.distanceTo(cannonPoint) <= MAX_DISTANCE) + { + Point cannonLoc = Perspective.getCanvasTextLocation(client, + graphics, + cannonPoint, + String.valueOf(plugin.getCballsLeft()), 150); + + if (cannonLoc != null) + { + textComponent.setText(String.valueOf(plugin.getCballsLeft())); + textComponent.setPosition(new java.awt.Point(cannonLoc.getX(), cannonLoc.getY())); + textComponent.setColor(plugin.getStateColor()); + textComponent.render(graphics); + } + + if (config.showDoubleHitSpot()) + { + Color color = config.highlightDoubleHitColor(); + drawDoubleHitSpots(graphics, cannonPoint, color); + } + } + + return null; + } + + + /** + * Draw the double hit spots on a 6 by 6 grid around the cannon + * @param startTile The position of the cannon + */ + private void drawDoubleHitSpots(Graphics2D graphics, LocalPoint startTile, Color color) + { + for (int x = -3; x <= 3; x++) + { + for (int y = -3; y <= 3; y++) + { + if (y != 1 && x != 1 && y != -1 && x != -1) + { + continue; + } + + //Ignore center square + if (y >= -1 && y <= 1 && x >= -1 && x <= 1) + { + continue; + } + + int xPos = startTile.getX() - (x * LOCAL_TILE_SIZE); + int yPos = startTile.getY() - (y * LOCAL_TILE_SIZE); + + LocalPoint marker = new LocalPoint(xPos, yPos); + Polygon poly = Perspective.getCanvasTilePoly(client, marker); + + if (poly == null) + { + continue; + } + + OverlayUtil.renderPolygon(graphics, poly, color); + } + } + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java new file mode 100644 index 0000000000..4d426ae9b6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2016-2018, Seth + * 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.cannon; + +import com.google.inject.Provides; +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; +import lombok.Getter; +import net.runelite.api.AnimationID; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.GameState; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; +import net.runelite.api.ItemID; +import static net.runelite.api.ObjectID.CANNON_BASE; +import net.runelite.api.Player; +import net.runelite.api.Projectile; +import static net.runelite.api.ProjectileID.CANNONBALL; +import static net.runelite.api.ProjectileID.GRANITE_CANNONBALL; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.ItemContainerChanged; +import net.runelite.api.events.ProjectileMoved; +import net.runelite.client.Notifier; +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.overlay.OverlayManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; + +@PluginDescriptor( + name = "Cannon", + description = "Show information about cannon placement and/or amount of cannonballs", + tags = {"combat", "notifications", "ranged", "overlay"} +) +public class CannonPlugin extends Plugin +{ + private static final Pattern NUMBER_PATTERN = Pattern.compile("([0-9]+)"); + static final int MAX_CBALLS = 30; + + private CannonCounter counter; + private boolean skipProjectileCheckThisTick; + private boolean cannonBallNotificationSent; + + @Getter + private int cballsLeft; + + @Getter + private boolean cannonPlaced; + + @Getter + private WorldPoint cannonPosition; + + @Getter + private int cannonWorld = -1; + + @Getter + private GameObject cannon; + + @Getter + private List spotPoints = new ArrayList<>(); + + @Inject + private ItemManager itemManager; + + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private Notifier notifier; + + @Inject + private OverlayManager overlayManager; + + @Inject + private CannonOverlay cannonOverlay; + + @Inject + private CannonSpotOverlay cannonSpotOverlay; + + @Inject + private CannonConfig config; + + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Provides + CannonConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(CannonConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(cannonOverlay); + overlayManager.add(cannonSpotOverlay); + } + + @Override + protected void shutDown() throws Exception + { + cannonSpotOverlay.setHidden(true); + overlayManager.remove(cannonOverlay); + overlayManager.remove(cannonSpotOverlay); + cannonPlaced = false; + cannonWorld = -1; + cannonPosition = null; + cannonBallNotificationSent = false; + cballsLeft = 0; + removeCounter(); + skipProjectileCheckThisTick = false; + spotPoints.clear(); + } + + @Subscribe + public void onItemContainerChanged(ItemContainerChanged event) + { + if (event.getItemContainer() != client.getItemContainer(InventoryID.INVENTORY)) + { + return; + } + + boolean hasBase = false; + boolean hasStand = false; + boolean hasBarrels = false; + boolean hasFurnace = false; + boolean hasAll = false; + + if (!cannonPlaced) + { + for (Item item : event.getItemContainer().getItems()) + { + if (item == null) + { + continue; + } + + switch (item.getId()) + { + case ItemID.CANNON_BASE: + hasBase = true; + break; + case ItemID.CANNON_STAND: + hasStand = true; + break; + case ItemID.CANNON_BARRELS: + hasBarrels = true; + break; + case ItemID.CANNON_FURNACE: + hasFurnace = true; + break; + } + + if (hasBase && hasStand && hasBarrels && hasFurnace) + { + hasAll = true; + break; + } + } + } + + cannonSpotOverlay.setHidden(!hasAll); + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals("cannon")) + { + if (!config.showInfobox()) + { + removeCounter(); + } + else + { + if (cannonPlaced) + { + clientThread.invoke(this::addCounter); + } + } + } + + } + + @Subscribe + public void onGameStateChanged(GameStateChanged gameStateChanged) + { + if (gameStateChanged.getGameState() != GameState.LOGGED_IN) + { + return; + } + + spotPoints.clear(); + for (WorldPoint spot : CannonSpots.getCannonSpots()) + { + if (WorldPoint.isInScene(client, spot.getX(), spot.getY())) + { + spotPoints.add(spot); + } + } + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned event) + { + GameObject gameObject = event.getGameObject(); + + Player localPlayer = client.getLocalPlayer(); + if (gameObject.getId() == CANNON_BASE && !cannonPlaced) + { + if (localPlayer.getWorldLocation().distanceTo(gameObject.getWorldLocation()) <= 2 + && localPlayer.getAnimation() == AnimationID.BURYING_BONES) + { + cannonPosition = gameObject.getWorldLocation(); + cannonWorld = client.getWorld(); + cannon = gameObject; + } + } + } + + @Subscribe + public void onProjectileMoved(ProjectileMoved event) + { + Projectile projectile = event.getProjectile(); + + if ((projectile.getId() == CANNONBALL || projectile.getId() == GRANITE_CANNONBALL) && cannonPosition != null && cannonWorld == client.getWorld()) + { + WorldPoint projectileLoc = WorldPoint.fromLocal(client, projectile.getX1(), projectile.getY1(), client.getPlane()); + + //Check to see if projectile x,y is 0 else it will continuously decrease while ball is flying. + if (projectileLoc.equals(cannonPosition) && projectile.getX() == 0 && projectile.getY() == 0) + { + // When there's a chat message about cannon reloaded/unloaded/out of ammo, + // the message event runs before the projectile event. However they run + // in the opposite order on the server. So if both fires in the same tick, + // we don't want to update the cannonball counter if it was set to a specific + // amount. + if (!skipProjectileCheckThisTick) + { + cballsLeft--; + + if (config.showCannonNotifications() && !cannonBallNotificationSent && cballsLeft > 0 && config.lowWarningThreshold() >= cballsLeft) + { + notifier.notify(String.format("Your cannon has %d cannon balls remaining!", cballsLeft)); + cannonBallNotificationSent = true; + } + } + } + } + } + + @Subscribe + public void onChatMessage(ChatMessage event) + { + if (event.getType() != ChatMessageType.SPAM && event.getType() != ChatMessageType.GAMEMESSAGE) + { + return; + } + + if (event.getMessage().equals("You add the furnace.")) + { + cannonPlaced = true; + addCounter(); + cballsLeft = 0; + } + + if (event.getMessage().contains("You pick up the cannon") + || event.getMessage().contains("Your cannon has decayed. Speak to Nulodion to get a new one!")) + { + cannonPlaced = false; + cballsLeft = 0; + removeCounter(); + } + + if (event.getMessage().startsWith("You load the cannon with")) + { + Matcher m = NUMBER_PATTERN.matcher(event.getMessage()); + if (m.find()) + { + // The cannon will usually refill to MAX_CBALLS, but if the + // player didn't have enough cannonballs in their inventory, + // it could fill up less than that. Filling the cannon to + // cballsLeft + amt is not always accurate though because our + // counter doesn't decrease if the player has been too far away + // from the cannon due to the projectiels not being in memory, + // so our counter can be higher than it is supposed to be. + int amt = Integer.valueOf(m.group()); + if (cballsLeft + amt >= MAX_CBALLS) + { + skipProjectileCheckThisTick = true; + cballsLeft = MAX_CBALLS; + } + else + { + cballsLeft += amt; + } + } + else if (event.getMessage().equals("You load the cannon with one cannonball.")) + { + if (cballsLeft + 1 >= MAX_CBALLS) + { + skipProjectileCheckThisTick = true; + cballsLeft = MAX_CBALLS; + } + else + { + cballsLeft++; + } + } + + cannonBallNotificationSent = false; + } + + if (event.getMessage().contains("Your cannon is out of ammo!")) + { + skipProjectileCheckThisTick = true; + + // If the player was out of range of the cannon, some cannonballs + // may have been used without the client knowing, so having this + // extra check is a good idea. + cballsLeft = 0; + + if (config.showCannonNotifications()) + { + notifier.notify("Your cannon is out of ammo!"); + } + } + + if (event.getMessage().startsWith("You unload your cannon and receive Cannonball") + || event.getMessage().startsWith("You unload your cannon and receive Granite cannonball")) + { + skipProjectileCheckThisTick = true; + + cballsLeft = 0; + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + skipProjectileCheckThisTick = false; + } + + Color getStateColor() + { + if (cballsLeft > 15) + { + return Color.green; + } + else if (cballsLeft > 5) + { + return Color.orange; + } + + return Color.red; + } + + private void addCounter() + { + if (!config.showInfobox() || counter != null) + { + return; + } + + counter = new CannonCounter(itemManager.getImage(ItemID.CANNONBALL), this); + counter.setTooltip("Cannonballs"); + + infoBoxManager.addInfoBox(counter); + } + + private void removeCounter() + { + if (counter == null) + { + return; + } + + infoBoxManager.removeInfoBox(counter); + counter = null; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpotOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpotOverlay.java new file mode 100644 index 0000000000..b7774855b5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpotOverlay.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018, Seth + * 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.cannon; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.image.BufferedImage; +import java.util.List; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Setter; +import net.runelite.api.Client; +import static net.runelite.api.ItemID.CANNONBALL; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; + +class CannonSpotOverlay extends Overlay +{ + private static final int MAX_DISTANCE = 2350; + + private final Client client; + private final CannonPlugin plugin; + private final CannonConfig config; + + @Inject + private ItemManager itemManager; + + @Setter(AccessLevel.PACKAGE) + private boolean hidden; + + @Inject + CannonSpotOverlay(Client client, CannonPlugin plugin, CannonConfig config) + { + setPosition(OverlayPosition.DYNAMIC); + this.client = client; + this.plugin = plugin; + this.config = config; + } + + @Override + public Dimension render(Graphics2D graphics) + { + List spotPoints = plugin.getSpotPoints(); + + if (hidden || spotPoints.isEmpty() || !config.showCannonSpots() || plugin.isCannonPlaced()) + { + return null; + } + + for (WorldPoint spot : spotPoints) + { + if (spot.getPlane() != client.getPlane()) + { + continue; + } + + LocalPoint spotPoint = LocalPoint.fromWorld(client, spot); + LocalPoint localLocation = client.getLocalPlayer().getLocalLocation(); + + if (spotPoint != null && localLocation.distanceTo(spotPoint) <= MAX_DISTANCE) + { + renderCannonSpot(graphics, client, spotPoint, itemManager.getImage(CANNONBALL), Color.RED); + } + } + + return null; + } + + private void renderCannonSpot(Graphics2D graphics, Client client, LocalPoint point, BufferedImage image, Color color) + { + //Render tile + Polygon poly = Perspective.getCanvasTilePoly(client, point); + + if (poly != null) + { + OverlayUtil.renderPolygon(graphics, poly, color); + } + + //Render icon + Point imageLoc = Perspective.getCanvasImageLocation(client, point, image, 0); + + if (imageLoc != null) + { + OverlayUtil.renderImageLocation(graphics, imageLoc, image); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpots.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpots.java new file mode 100644 index 0000000000..f5b6f4a4a2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonSpots.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018, Seth + * 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.cannon; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import net.runelite.api.coords.WorldPoint; + +enum CannonSpots +{ + ABERRANT_SPECTRES(new WorldPoint(2456, 9791, 0)), + ANKOU(new WorldPoint(3177, 10193, 0)), + BANDIT(new WorldPoint(3037, 3700, 0)), + BEAR(new WorldPoint(3113, 3672, 0)), + BLACK_DEMONS(new WorldPoint(2859, 9778, 0), new WorldPoint(2841, 9791, 0), new WorldPoint(1421, 10089, 1), new WorldPoint(3174, 10154, 0), new WorldPoint(3089, 9960, 0)), + BLACK_DRAGON(new WorldPoint(3239, 10206, 0)), + BLACK_KNIGHTS(new WorldPoint(2906, 9685, 0), new WorldPoint(3053, 3852, 0)), + BLOODVELDS(new WorldPoint(2439, 9821, 0), new WorldPoint(2448, 9821, 0), new WorldPoint(2472, 9832, 0), new WorldPoint(2453, 9817, 0), new WorldPoint(3597, 9743, 0)), + BLUE_DRAGON(new WorldPoint(1933, 8973, 1)), + BRINE_RAT(new WorldPoint(2707, 10132, 0)), + CAVE_HORROR(new WorldPoint(3785, 9460, 0)), + DAGGANOTH(new WorldPoint(2524, 10020, 0)), + DARK_BEAST(new WorldPoint(1992, 4655, 0)), + DARK_WARRIOR(new WorldPoint(3030, 3632, 0)), + DUST_DEVIL(new WorldPoint(3218, 9366, 0)), + EARTH_WARRIOR(new WorldPoint(3120, 9987, 0)), + ELDER_CHAOS_DRUID(new WorldPoint(3237, 3622, 0)), + ELVES(new WorldPoint(2044, 4635, 0), new WorldPoint(3278, 6098, 0)), + FIRE_GIANTS(new WorldPoint(2393, 9782, 0), new WorldPoint(2412, 9776, 0), new WorldPoint(2401, 9780, 0), new WorldPoint(3047, 10340, 0)), + GREATER_DEMONS(new WorldPoint(1435, 10086, 2), new WorldPoint(3224, 10132, 0)), + GREEN_DRAGON(new WorldPoint(3225, 10068, 0)), + HELLHOUNDS(new WorldPoint(2431, 9776, 0), new WorldPoint(2413, 9786, 0), new WorldPoint(2783, 9686, 0), new WorldPoint(3198, 10071, 0)), + HILL_GIANT(new WorldPoint(3044, 10318, 0)), + ICE_GIANT(new WorldPoint(3207, 10164, 0)), + ICE_WARRIOR(new WorldPoint(2955, 3876, 0)), + KALPHITE(new WorldPoint(3307, 9528, 0)), + LESSER_DEMON(new WorldPoint(2838, 9559, 0), new WorldPoint(3163, 10114, 0)), + LIZARDMEN(new WorldPoint(1500, 3703, 0)), + LIZARDMEN_SHAMAN(new WorldPoint(1423, 3715, 0)), + MAGIC_AXE(new WorldPoint(3190, 3960, 0)), + MAMMOTH(new WorldPoint(3168, 3595, 0)), + MINIONS_OF_SCARABAS(new WorldPoint(3297, 9252, 0)), + ROGUE(new WorldPoint(3285, 3930, 0)), + SCORPION(new WorldPoint(3233, 10335, 0)), + SKELETON(new WorldPoint(3018, 3592, 0)), + SMOKE_DEVIL(new WorldPoint(2398, 9444, 0)), + SPIDER(new WorldPoint(3169, 3886, 0)), + SUQAHS(new WorldPoint(2114, 3943, 0)), + TROLLS(new WorldPoint(2401, 3856, 0), new WorldPoint(1242, 3517, 0)), + ZOMBIE(new WorldPoint(3172, 3677, 0)); + + @Getter + private static final List cannonSpots = new ArrayList<>(); + + static + { + for (CannonSpots cannonSpot : values()) + { + cannonSpots.addAll(Arrays.asList(cannonSpot.spots)); + } + } + + private final WorldPoint[] spots; + + CannonSpots(WorldPoint... spots) + { + this.spots = spots; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java new file mode 100644 index 0000000000..da642e1d43 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2018, Woox + * 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.chatboxperformance; + +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.ScriptID; +import net.runelite.api.events.ScriptCallbackEvent; +import net.runelite.api.widgets.WidgetType; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.api.widgets.WidgetPositionMode; +import net.runelite.api.widgets.WidgetSizeMode; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; + +@PluginDescriptor( + name = "Chatbox performance", + hidden = true +) +public class ChatboxPerformancePlugin extends Plugin +{ + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Override + public void startUp() + { + if (client.getGameState() == GameState.LOGGED_IN) + { + clientThread.invokeLater(() -> client.runScript(ScriptID.MESSAGE_LAYER_CLOSE, 0, 0)); + } + } + + @Override + public void shutDown() + { + if (client.getGameState() == GameState.LOGGED_IN) + { + clientThread.invokeLater(() -> client.runScript(ScriptID.MESSAGE_LAYER_CLOSE, 0, 0)); + } + } + + @Subscribe + private void onScriptCallbackEvent(ScriptCallbackEvent ev) + { + if (!"chatboxBackgroundBuilt".equals(ev.getEventName())) + { + return; + } + + fixDarkBackground(); + fixWhiteLines(true); + fixWhiteLines(false); + } + + private void fixDarkBackground() + { + int currOpacity = 256; + int prevY = 0; + Widget[] children = client.getWidget(WidgetInfo.CHATBOX_TRANSPARENT_BACKGROUND).getDynamicChildren(); + Widget prev = null; + for (Widget w : children) + { + if (w.getType() != WidgetType.RECTANGLE) + { + continue; + } + + if (prev != null) + { + int relY = w.getRelativeY(); + prev.setHeightMode(WidgetSizeMode.ABSOLUTE); + prev.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + prev.setRelativeY(prevY); + prev.setOriginalY(prev.getRelativeY()); + prev.setHeight(relY - prevY); + prev.setOriginalHeight(prev.getHeight()); + prev.setOpacity(currOpacity); + } + + prevY = w.getRelativeY(); + currOpacity -= 3; // Rough number, can't get exactly the same as Jagex because of rounding + prev = w; + } + if (prev != null) + { + prev.setOpacity(currOpacity); + } + } + + private void fixWhiteLines(boolean upperLine) + { + int currOpacity = 256; + int prevWidth = 0; + Widget[] children = client.getWidget(WidgetInfo.CHATBOX_TRANSPARENT_LINES).getDynamicChildren(); + Widget prev = null; + for (Widget w : children) + { + if (w.getType() != WidgetType.RECTANGLE) + { + continue; + } + + if ((w.getRelativeY() == 0 && !upperLine) || + (w.getRelativeY() != 0 && upperLine)) + { + continue; + } + + if (prev != null) + { + int width = w.getWidth(); + prev.setWidthMode(WidgetSizeMode.ABSOLUTE); + prev.setRelativeX(width); + prev.setOriginalX(width); + prev.setWidth(prevWidth - width); + prev.setOriginalWidth(prev.getWidth()); + prev.setOpacity(currOpacity); + } + + prevWidth = w.getWidth(); + + currOpacity -= upperLine ? 3 : 4; // Rough numbers, can't get exactly the same as Jagex because of rounding + prev = w; + } + if (prev != null) + { + prev.setOpacity(currOpacity); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java new file mode 100644 index 0000000000..4a8d46bd4e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2017, 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.chatcommands; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Keybind; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +@ConfigGroup("chatcommands") +public interface ChatCommandsConfig extends Config +{ + @ConfigItem( + position = 0, + keyName = "price", + name = "Price Command", + description = "Configures whether the Price command is enabled
!price [item]" + ) + default boolean price() + { + return true; + } + + @ConfigItem( + position = 1, + keyName = "lvl", + name = "Level Command", + description = "Configures whether the Level command is enabled
!lvl [skill]" + ) + default boolean lvl() + { + return true; + } + + @ConfigItem( + position = 2, + keyName = "clue", + name = "Clue Command", + description = "Configures whether the Clue command is enabled
!clues" + ) + default boolean clue() + { + return true; + } + + @ConfigItem( + position = 3, + keyName = "killcount", + name = "Killcount Command", + description = "Configures whether the Killcount command is enabled
!kc [boss]" + ) + default boolean killcount() + { + return true; + } + + @ConfigItem( + position = 4, + keyName = "qp", + name = "QP Command", + description = "Configures whether the quest point command is enabled
!qp" + ) + default boolean qp() + { + return true; + } + + @ConfigItem( + position = 5, + keyName = "pb", + name = "PB Command", + description = "Configures whether the personal best command is enabled
!pb" + ) + default boolean pb() + { + return true; + } + + @ConfigItem( + position = 6, + keyName = "gc", + name = "GC Command", + description = "Configures whether the Barbarian Assault High gamble count command is enabled
!gc" + ) + default boolean gc() + { + return true; + } + + @ConfigItem( + position = 7, + keyName = "duels", + name = "Duels Command", + description = "Configures whether the duel arena command is enabled
!duels" + ) + default boolean duels() + { + return true; + } + + @ConfigItem( + position = 8, + keyName = "bh", + name = "BH Command", + description = "Configures whether the Bounty Hunter - Hunter command is enabled
!bh" + ) + default boolean bh() + { + return true; + } + + @ConfigItem( + position = 9, + keyName = "bhRogue", + name = "BH Rogue Command", + description = "Configures whether the Bounty Hunter - Rogue command is enabled
!bhrogue" + ) + default boolean bhRogue() + { + return true; + } + + @ConfigItem( + position = 10, + keyName = "lms", + name = "LMS Command", + description = "Configures whether the Last Man Standing command is enabled
!lms" + ) + default boolean lms() + { + return true; + } + + @ConfigItem( + position = 11, + keyName = "lp", + name = "LP Command", + description = "Configures whether the League Points command is enabled
!lp" + ) + default boolean lp() + { + return true; + } + + @ConfigItem( + position = 12, + keyName = "clearSingleWord", + name = "Clear Single Word", + description = "Enable hot key to clear single word at a time" + ) + default Keybind clearSingleWord() + { + return new Keybind(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK); + } + + @ConfigItem( + position = 13, + keyName = "clearEntireChatBox", + name = "Clear Chat Box", + description = "Enable hotkey to clear entire chat box" + ) + default Keybind clearChatBox() + { + return new Keybind(KeyEvent.VK_BACK_SPACE, InputEvent.CTRL_DOWN_MASK); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java new file mode 100644 index 0000000000..4259d0ee75 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java @@ -0,0 +1,1846 @@ +/* + * Copyright (c) 2017. l2- + * Copyright (c) 2017, 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.chatcommands; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.inject.Provides; +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.Experience; +import net.runelite.api.IconID; +import net.runelite.api.ItemComposition; +import net.runelite.api.MessageNode; +import net.runelite.api.Player; +import net.runelite.api.VarPlayer; +import net.runelite.api.Varbits; +import net.runelite.api.WorldType; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.VarbitChanged; +import net.runelite.api.events.WidgetLoaded; +import net.runelite.api.vars.AccountType; +import net.runelite.api.widgets.Widget; +import static net.runelite.api.widgets.WidgetID.ADVENTURE_LOG_ID; +import static net.runelite.api.widgets.WidgetID.GENERIC_SCROLL_GROUP_ID; +import static net.runelite.api.widgets.WidgetID.KILL_LOGS_GROUP_ID; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.chat.ChatColorType; +import net.runelite.client.chat.ChatCommandManager; +import net.runelite.client.chat.ChatMessageBuilder; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ChatInput; +import net.runelite.client.game.ItemManager; +import net.runelite.client.input.KeyManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.QuantityFormatter; +import net.runelite.client.util.Text; +import net.runelite.http.api.chat.ChatClient; +import net.runelite.http.api.chat.Duels; +import net.runelite.http.api.hiscore.HiscoreClient; +import net.runelite.http.api.hiscore.HiscoreEndpoint; +import net.runelite.http.api.hiscore.HiscoreResult; +import net.runelite.http.api.hiscore.HiscoreSkill; +import net.runelite.http.api.hiscore.SingleHiscoreSkillResult; +import net.runelite.http.api.hiscore.Skill; +import net.runelite.http.api.item.ItemPrice; +import okhttp3.OkHttpClient; +import org.apache.commons.text.WordUtils; + +@PluginDescriptor( + name = "Chat Commands", + description = "Enable chat commands", + tags = {"grand", "exchange", "level", "prices"} +) +@Slf4j +public class ChatCommandsPlugin extends Plugin +{ + private static final Pattern KILLCOUNT_PATTERN = Pattern.compile("Your (.+) (?:kill|harvest|lap|completion) count is: (\\d+)"); + private static final Pattern RAIDS_PATTERN = Pattern.compile("Your completed (.+) count is: (\\d+)"); + private static final String COX_TEAM_SIZES = "(?:\\d+(?:\\+|-\\d+)? players|Solo)"; + private static final Pattern RAIDS_PB_PATTERN = Pattern.compile("Congratulations - your raid is complete!
Team size: " + COX_TEAM_SIZES + " Duration: (?[0-9:]+) \\(new personal best\\)"); + private static final Pattern RAIDS_DURATION_PATTERN = Pattern.compile("Congratulations - your raid is complete!
Team size: " + COX_TEAM_SIZES + " Duration: [0-9:]+ Personal best: (?[0-9:]+)"); + private static final Pattern TOB_WAVE_PB_PATTERN = Pattern.compile("^.*Theatre of Blood wave completion time: (?[0-9:]+) \\(Personal best!\\)"); + private static final Pattern TOB_WAVE_DURATION_PATTERN = Pattern.compile("^.*Theatre of Blood wave completion time: [0-9:]+
Personal best: (?[0-9:]+)"); + private static final Pattern WINTERTODT_PATTERN = Pattern.compile("Your subdued Wintertodt count is: (\\d+)"); + private static final Pattern BARROWS_PATTERN = Pattern.compile("Your Barrows chest count is: (\\d+)"); + private static final Pattern KILL_DURATION_PATTERN = Pattern.compile("(?i)^(?:Fight |Lap |Challenge |Corrupted challenge )?duration: [0-9:]+\\. Personal best: (?[0-9:]+)"); + private static final Pattern NEW_PB_PATTERN = Pattern.compile("(?i)^(?:Fight |Lap |Challenge |Corrupted challenge )?duration: (?[0-9:]+) \\(new personal best\\)"); + private static final Pattern DUEL_ARENA_WINS_PATTERN = Pattern.compile("You (were defeated|won)! You have(?: now)? won (\\d+) duels?"); + private static final Pattern DUEL_ARENA_LOSSES_PATTERN = Pattern.compile("You have(?: now)? lost (\\d+) duels?"); + private static final Pattern ADVENTURE_LOG_TITLE_PATTERN = Pattern.compile("The Exploits of (.+)"); + private static final Pattern ADVENTURE_LOG_COX_PB_PATTERN = Pattern.compile("Fastest (?:kill|run)(?: - \\(Team size: " + COX_TEAM_SIZES + "\\))?: ([0-9:]+)"); + private static final Pattern ADVENTURE_LOG_BOSS_PB_PATTERN = Pattern.compile("[a-zA-Z]+(?: [a-zA-Z]+)*"); + private static final Pattern ADVENTURE_LOG_PB_PATTERN = Pattern.compile("(" + ADVENTURE_LOG_BOSS_PB_PATTERN + "(?: - " + ADVENTURE_LOG_BOSS_PB_PATTERN + ")*) (?:" + ADVENTURE_LOG_COX_PB_PATTERN + "( )*)+"); + private static final Pattern HS_PB_PATTERN = Pattern.compile("Floor (?\\d) time: (?[0-9:]+)(?: \\(new personal best\\)|. Personal best: (?[0-9:]+))" + + "(?:
Overall time: (?[0-9:]+)(?: \\(new personal best\\)|. Personal best: (?[0-9:]+)))?"); + private static final Pattern HS_KC_FLOOR_PATTERN = Pattern.compile("You have completed Floor (\\d) of the Hallowed Sepulchre! Total completions: (\\d+)\\."); + private static final Pattern HS_KC_GHC_PATTERN = Pattern.compile("You have opened the Grand Hallowed Coffin (\\d+) times?!"); + + private static final String TOTAL_LEVEL_COMMAND_STRING = "!total"; + private static final String PRICE_COMMAND_STRING = "!price"; + private static final String LEVEL_COMMAND_STRING = "!lvl"; + private static final String BOUNTY_HUNTER_HUNTER_COMMAND = "!bh"; + private static final String BOUNTY_HUNTER_ROGUE_COMMAND = "!bhrogue"; + private static final String CLUES_COMMAND_STRING = "!clues"; + private static final String LAST_MAN_STANDING_COMMAND = "!lms"; + private static final String KILLCOUNT_COMMAND_STRING = "!kc"; + private static final String CMB_COMMAND_STRING = "!cmb"; + private static final String QP_COMMAND_STRING = "!qp"; + private static final String PB_COMMAND = "!pb"; + private static final String GC_COMMAND_STRING = "!gc"; + private static final String DUEL_ARENA_COMMAND = "!duels"; + private static final String LEAGUE_POINTS_COMMAND = "!lp"; + + @VisibleForTesting + static final int ADV_LOG_EXPLOITS_TEXT_INDEX = 1; + + private boolean bossLogLoaded; + private boolean advLogLoaded; + private boolean scrollInterfaceLoaded; + private String pohOwner; + private HiscoreEndpoint hiscoreEndpoint; // hiscore endpoint for current player + private String lastBossKill; + private int lastBossTime = -1; + private int lastPb = -1; + + @Inject + private Client client; + + @Inject + private ChatCommandsConfig config; + + @Inject + private ConfigManager configManager; + + @Inject + private ItemManager itemManager; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private ChatCommandManager chatCommandManager; + + @Inject + private ScheduledExecutorService executor; + + @Inject + private KeyManager keyManager; + + @Inject + private ChatKeyboardListener chatKeyboardListener; + + @Inject + private HiscoreClient hiscoreClient; + + @Inject + private ChatClient chatClient; + + @Override + public void startUp() + { + keyManager.registerKeyListener(chatKeyboardListener); + + chatCommandManager.registerCommandAsync(TOTAL_LEVEL_COMMAND_STRING, this::playerSkillLookup); + chatCommandManager.registerCommandAsync(CMB_COMMAND_STRING, this::combatLevelLookup); + chatCommandManager.registerCommand(PRICE_COMMAND_STRING, this::itemPriceLookup); + chatCommandManager.registerCommandAsync(LEVEL_COMMAND_STRING, this::playerSkillLookup); + chatCommandManager.registerCommandAsync(BOUNTY_HUNTER_HUNTER_COMMAND, this::bountyHunterHunterLookup); + chatCommandManager.registerCommandAsync(BOUNTY_HUNTER_ROGUE_COMMAND, this::bountyHunterRogueLookup); + chatCommandManager.registerCommandAsync(CLUES_COMMAND_STRING, this::clueLookup); + chatCommandManager.registerCommandAsync(LAST_MAN_STANDING_COMMAND, this::lastManStandingLookup); + chatCommandManager.registerCommandAsync(LEAGUE_POINTS_COMMAND, this::leaguePointsLookup); + chatCommandManager.registerCommandAsync(KILLCOUNT_COMMAND_STRING, this::killCountLookup, this::killCountSubmit); + chatCommandManager.registerCommandAsync(QP_COMMAND_STRING, this::questPointsLookup, this::questPointsSubmit); + chatCommandManager.registerCommandAsync(PB_COMMAND, this::personalBestLookup, this::personalBestSubmit); + chatCommandManager.registerCommandAsync(GC_COMMAND_STRING, this::gambleCountLookup, this::gambleCountSubmit); + chatCommandManager.registerCommandAsync(DUEL_ARENA_COMMAND, this::duelArenaLookup, this::duelArenaSubmit); + } + + @Override + public void shutDown() + { + lastBossKill = null; + lastBossTime = -1; + + keyManager.unregisterKeyListener(chatKeyboardListener); + + chatCommandManager.unregisterCommand(TOTAL_LEVEL_COMMAND_STRING); + chatCommandManager.unregisterCommand(CMB_COMMAND_STRING); + chatCommandManager.unregisterCommand(PRICE_COMMAND_STRING); + chatCommandManager.unregisterCommand(LEVEL_COMMAND_STRING); + chatCommandManager.unregisterCommand(BOUNTY_HUNTER_HUNTER_COMMAND); + chatCommandManager.unregisterCommand(BOUNTY_HUNTER_ROGUE_COMMAND); + chatCommandManager.unregisterCommand(CLUES_COMMAND_STRING); + chatCommandManager.unregisterCommand(LAST_MAN_STANDING_COMMAND); + chatCommandManager.unregisterCommand(LEAGUE_POINTS_COMMAND); + chatCommandManager.unregisterCommand(KILLCOUNT_COMMAND_STRING); + chatCommandManager.unregisterCommand(QP_COMMAND_STRING); + chatCommandManager.unregisterCommand(PB_COMMAND); + chatCommandManager.unregisterCommand(GC_COMMAND_STRING); + chatCommandManager.unregisterCommand(DUEL_ARENA_COMMAND); + } + + @Provides + ChatCommandsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(ChatCommandsConfig.class); + } + + @Provides + HiscoreClient provideHiscoreClient(OkHttpClient okHttpClient) + { + return new HiscoreClient(okHttpClient); + } + + private void setKc(String boss, int killcount) + { + configManager.setRSProfileConfiguration("killcount", boss.toLowerCase(), killcount); + } + + private int getKc(String boss) + { + Integer killCount = configManager.getRSProfileConfiguration("killcount", boss.toLowerCase(), int.class); + return killCount == null ? 0 : killCount; + } + + private void setPb(String boss, int seconds) + { + configManager.setRSProfileConfiguration("personalbest", boss.toLowerCase(), seconds); + } + + private int getPb(String boss) + { + Integer personalBest = configManager.getRSProfileConfiguration("personalbest", boss.toLowerCase(), int.class); + return personalBest == null ? 0 : personalBest; + } + + @Subscribe + public void onChatMessage(ChatMessage chatMessage) + { + if (chatMessage.getType() != ChatMessageType.TRADE + && chatMessage.getType() != ChatMessageType.GAMEMESSAGE + && chatMessage.getType() != ChatMessageType.SPAM + && chatMessage.getType() != ChatMessageType.FRIENDSCHATNOTIFICATION) + { + return; + } + + String message = chatMessage.getMessage(); + Matcher matcher = KILLCOUNT_PATTERN.matcher(message); + if (matcher.find()) + { + String boss = matcher.group(1); + int kc = Integer.parseInt(matcher.group(2)); + + setKc(boss, kc); + // We either already have the pb, or need to remember the boss for the upcoming pb + if (lastPb > -1) + { + log.debug("Got out-of-order personal best for {}: {}", boss, lastPb); + setPb(boss, lastPb); + lastPb = -1; + } + else + { + lastBossKill = boss; + lastBossTime = client.getTickCount(); + } + return; + } + + matcher = WINTERTODT_PATTERN.matcher(message); + if (matcher.find()) + { + int kc = Integer.parseInt(matcher.group(1)); + + setKc("Wintertodt", kc); + } + + matcher = RAIDS_PATTERN.matcher(message); + if (matcher.find()) + { + String boss = matcher.group(1); + int kc = Integer.parseInt(matcher.group(2)); + + setKc(boss, kc); + if (lastPb > -1) + { + setPb(boss, lastPb); + lastPb = -1; + } + lastBossKill = boss; + lastBossTime = client.getTickCount(); + return; + } + + matcher = DUEL_ARENA_WINS_PATTERN.matcher(message); + if (matcher.find()) + { + final int oldWins = getKc("Duel Arena Wins"); + final int wins = Integer.parseInt(matcher.group(2)); + final String result = matcher.group(1); + int winningStreak = getKc("Duel Arena Win Streak"); + int losingStreak = getKc("Duel Arena Lose Streak"); + + if (result.equals("won") && wins > oldWins) + { + losingStreak = 0; + winningStreak += 1; + } + else if (result.equals("were defeated")) + { + losingStreak += 1; + winningStreak = 0; + } + else + { + log.warn("unrecognized duel streak chat message: {}", message); + } + + setKc("Duel Arena Wins", wins); + setKc("Duel Arena Win Streak", winningStreak); + setKc("Duel Arena Lose Streak", losingStreak); + } + + matcher = DUEL_ARENA_LOSSES_PATTERN.matcher(message); + if (matcher.find()) + { + int losses = Integer.parseInt(matcher.group(1)); + + setKc("Duel Arena Losses", losses); + } + + matcher = BARROWS_PATTERN.matcher(message); + if (matcher.find()) + { + int kc = Integer.parseInt(matcher.group(1)); + + setKc("Barrows Chests", kc); + } + + matcher = KILL_DURATION_PATTERN.matcher(message); + if (matcher.find()) + { + matchPb(matcher); + } + + matcher = NEW_PB_PATTERN.matcher(message); + if (matcher.find()) + { + matchPb(matcher); + } + + matcher = RAIDS_PB_PATTERN.matcher(message); + if (matcher.find()) + { + matchPb(matcher); + } + + matcher = RAIDS_DURATION_PATTERN.matcher(message); + if (matcher.find()) + { + matchPb(matcher); + } + + matcher = TOB_WAVE_PB_PATTERN.matcher(message); + if (matcher.find()) + { + matchPb(matcher); + } + + matcher = TOB_WAVE_DURATION_PATTERN.matcher(message); + if (matcher.find()) + { + matchPb(matcher); + } + + matcher = HS_PB_PATTERN.matcher(message); + if (matcher.find()) + { + int floor = Integer.parseInt(matcher.group("floor")); + String floortime = matcher.group("floortime"); + String floorpb = matcher.group("floorpb"); + String otime = matcher.group("otime"); + String opb = matcher.group("opb"); + + String pb = MoreObjects.firstNonNull(floorpb, floortime); + setPb("Hallowed Sepulchre Floor " + floor, timeStringToSeconds(pb)); + + if (otime != null) + { + pb = MoreObjects.firstNonNull(opb, otime); + setPb("Hallowed Sepulchre", timeStringToSeconds(pb)); + } + } + + matcher = HS_KC_FLOOR_PATTERN.matcher(message); + if (matcher.find()) + { + int floor = Integer.parseInt(matcher.group(1)); + int kc = Integer.parseInt(matcher.group(2)); + setKc("Hallowed Sepulchre Floor " + floor, kc); + } + + matcher = HS_KC_GHC_PATTERN.matcher(message); + if (matcher.find()) + { + int kc = Integer.parseInt(matcher.group(1)); + setKc("Hallowed Sepulchre", kc); + } + + if (lastBossKill != null && lastBossTime != client.getTickCount()) + { + lastBossKill = null; + lastBossTime = -1; + } + } + + private static int timeStringToSeconds(String timeString) + { + String[] s = timeString.split(":"); + if (s.length == 2) // mm:ss + { + return Integer.parseInt(s[0]) * 60 + Integer.parseInt(s[1]); + } + else if (s.length == 3) // h:mm:ss + { + return Integer.parseInt(s[0]) * 60 * 60 + Integer.parseInt(s[1]) * 60 + Integer.parseInt(s[2]); + } + return Integer.parseInt(timeString); + } + + private void matchPb(Matcher matcher) + { + int seconds = timeStringToSeconds(matcher.group("pb")); + if (lastBossKill != null) + { + // Most bosses sent boss kill message, and then pb message, so we + // use the remembered lastBossKill + log.debug("Got personal best for {}: {}", lastBossKill, seconds); + setPb(lastBossKill, seconds); + lastPb = -1; + } + else + { + // Some bosses send the pb message, and then the kill message! + lastPb = seconds; + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + if (client.getLocalPlayer() == null) + { + return; + } + + if (advLogLoaded) + { + advLogLoaded = false; + + Widget adventureLog = client.getWidget(WidgetInfo.ADVENTURE_LOG); + Matcher advLogExploitsText = ADVENTURE_LOG_TITLE_PATTERN.matcher(adventureLog.getChild(ADV_LOG_EXPLOITS_TEXT_INDEX).getText()); + if (advLogExploitsText.find()) + { + pohOwner = advLogExploitsText.group(1); + } + } + + if (bossLogLoaded && (pohOwner == null || pohOwner.equals(client.getLocalPlayer().getName()))) + { + bossLogLoaded = false; + + Widget title = client.getWidget(WidgetInfo.KILL_LOG_TITLE); + Widget bossMonster = client.getWidget(WidgetInfo.KILL_LOG_MONSTER); + Widget bossKills = client.getWidget(WidgetInfo.KILL_LOG_KILLS); + + if (title == null || bossMonster == null || bossKills == null + || !"Boss Kill Log".equals(title.getText())) + { + return; + } + + Widget[] bossChildren = bossMonster.getChildren(); + Widget[] killsChildren = bossKills.getChildren(); + + for (int i = 0; i < bossChildren.length; ++i) + { + Widget boss = bossChildren[i]; + Widget kill = killsChildren[i]; + + String bossName = boss.getText().replace(":", ""); + int kc = Integer.parseInt(kill.getText().replace(",", "")); + if (kc != getKc(longBossName(bossName))) + { + setKc(longBossName(bossName), kc); + } + } + } + + if (scrollInterfaceLoaded) + { + scrollInterfaceLoaded = false; + + if (client.getLocalPlayer().getName().equals(pohOwner)) + { + String counterText = Text.sanitizeMultilineText(client.getWidget(WidgetInfo.GENERIC_SCROLL_TEXT).getText()); + Matcher mCounterText = ADVENTURE_LOG_PB_PATTERN.matcher(counterText); + while (mCounterText.find()) + { + String bossName = longBossName(mCounterText.group(1)); + if (bossName.equalsIgnoreCase("chambers of xeric") || + bossName.equalsIgnoreCase("chambers of xeric challenge mode")) + { + Matcher mCoxRuns = ADVENTURE_LOG_COX_PB_PATTERN.matcher(mCounterText.group()); + int bestPbTime = Integer.MAX_VALUE; + while (mCoxRuns.find()) + { + bestPbTime = Math.min(timeStringToSeconds(mCoxRuns.group(1)), bestPbTime); + } + // So we don't reset people's already saved PB's if they had one before the update + int currentPb = getPb(bossName); + if (currentPb == 0 || currentPb > bestPbTime) + { + setPb(bossName, bestPbTime); + } + } + else + { + String pbTime = mCounterText.group(2); + setPb(bossName, timeStringToSeconds(pbTime)); + } + } + } + } + } + + @Subscribe + public void onWidgetLoaded(WidgetLoaded widget) + { + switch (widget.getGroupId()) + { + case ADVENTURE_LOG_ID: + advLogLoaded = true; + break; + case KILL_LOGS_GROUP_ID: + bossLogLoaded = true; + break; + case GENERIC_SCROLL_GROUP_ID: + scrollInterfaceLoaded = true; + break; + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case LOADING: + case HOPPING: + pohOwner = null; + } + } + + @Subscribe + public void onVarbitChanged(VarbitChanged varbitChanged) + { + hiscoreEndpoint = getLocalHiscoreEndpointType(); + } + + private boolean killCountSubmit(ChatInput chatInput, String value) + { + int idx = value.indexOf(' '); + final String boss = longBossName(value.substring(idx + 1)); + + final int kc = getKc(boss); + if (kc <= 0) + { + return false; + } + + final String playerName = client.getLocalPlayer().getName(); + + executor.execute(() -> + { + try + { + chatClient.submitKc(playerName, boss, kc); + } + catch (Exception ex) + { + log.warn("unable to submit killcount", ex); + } + finally + { + chatInput.resume(); + } + }); + + return true; + } + + private void killCountLookup(ChatMessage chatMessage, String message) + { + if (!config.killcount()) + { + return; + } + + if (message.length() <= KILLCOUNT_COMMAND_STRING.length()) + { + return; + } + + ChatMessageType type = chatMessage.getType(); + String search = message.substring(KILLCOUNT_COMMAND_STRING.length() + 1); + + final String player; + if (type.equals(ChatMessageType.PRIVATECHATOUT)) + { + player = client.getLocalPlayer().getName(); + } + else + { + player = Text.sanitize(chatMessage.getName()); + } + + search = longBossName(search); + + final int kc; + try + { + kc = chatClient.getKc(player, search); + } + catch (IOException ex) + { + log.debug("unable to lookup killcount", ex); + return; + } + + String response = new ChatMessageBuilder() + .append(ChatColorType.HIGHLIGHT) + .append(search) + .append(ChatColorType.NORMAL) + .append(" kill count: ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(kc)) + .build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + + private boolean duelArenaSubmit(ChatInput chatInput, String value) + { + final int wins = getKc("Duel Arena Wins"); + final int losses = getKc("Duel Arena Losses"); + final int winningStreak = getKc("Duel Arena Win Streak"); + final int losingStreak = getKc("Duel Arena Lose Streak"); + + if (wins <= 0 && losses <= 0 && winningStreak <= 0 && losingStreak <= 0) + { + return false; + } + + final String playerName = client.getLocalPlayer().getName(); + + executor.execute(() -> + { + try + { + chatClient.submitDuels(playerName, wins, losses, winningStreak, losingStreak); + } + catch (Exception ex) + { + log.warn("unable to submit duels", ex); + } + finally + { + chatInput.resume(); + } + }); + + return true; + } + + private void duelArenaLookup(ChatMessage chatMessage, String message) + { + if (!config.duels()) + { + return; + } + + ChatMessageType type = chatMessage.getType(); + + final String player; + if (type == ChatMessageType.PRIVATECHATOUT) + { + player = client.getLocalPlayer().getName(); + } + else + { + player = Text.sanitize(chatMessage.getName()); + } + + Duels duels; + try + { + duels = chatClient.getDuels(player); + } + catch (IOException ex) + { + log.debug("unable to lookup duels", ex); + return; + } + + final int wins = duels.getWins(); + final int losses = duels.getLosses(); + final int winningStreak = duels.getWinningStreak(); + final int losingStreak = duels.getLosingStreak(); + + String response = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Duel Arena wins: ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(wins)) + .append(ChatColorType.NORMAL) + .append(" losses: ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(losses)) + .append(ChatColorType.NORMAL) + .append(" streak: ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString((winningStreak != 0 ? winningStreak : -losingStreak))) + .build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + + private void questPointsLookup(ChatMessage chatMessage, String message) + { + if (!config.qp()) + { + return; + } + + ChatMessageType type = chatMessage.getType(); + + final String player; + if (type.equals(ChatMessageType.PRIVATECHATOUT)) + { + player = client.getLocalPlayer().getName(); + } + else + { + player = Text.sanitize(chatMessage.getName()); + } + + int qp; + try + { + qp = chatClient.getQp(player); + } + catch (IOException ex) + { + log.debug("unable to lookup quest points", ex); + return; + } + + String response = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Quest points: ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(qp)) + .build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + + private boolean questPointsSubmit(ChatInput chatInput, String value) + { + final int qp = client.getVar(VarPlayer.QUEST_POINTS); + final String playerName = client.getLocalPlayer().getName(); + + executor.execute(() -> + { + try + { + chatClient.submitQp(playerName, qp); + } + catch (Exception ex) + { + log.warn("unable to submit quest points", ex); + } + finally + { + chatInput.resume(); + } + }); + + return true; + } + + private void personalBestLookup(ChatMessage chatMessage, String message) + { + if (!config.pb()) + { + return; + } + + if (message.length() <= PB_COMMAND.length()) + { + return; + } + + ChatMessageType type = chatMessage.getType(); + String search = message.substring(PB_COMMAND.length() + 1); + + final String player; + if (type.equals(ChatMessageType.PRIVATECHATOUT)) + { + player = client.getLocalPlayer().getName(); + } + else + { + player = Text.sanitize(chatMessage.getName()); + } + + search = longBossName(search); + + final int pb; + try + { + pb = chatClient.getPb(player, search); + } + catch (IOException ex) + { + log.debug("unable to lookup personal best", ex); + return; + } + + int minutes = pb / 60; + int seconds = pb % 60; + + String response = new ChatMessageBuilder() + .append(ChatColorType.HIGHLIGHT) + .append(search) + .append(ChatColorType.NORMAL) + .append(" personal best: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.format("%d:%02d", minutes, seconds)) + .build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + + private boolean personalBestSubmit(ChatInput chatInput, String value) + { + int idx = value.indexOf(' '); + final String boss = longBossName(value.substring(idx + 1)); + + final int pb = getPb(boss); + if (pb <= 0) + { + return false; + } + + final String playerName = client.getLocalPlayer().getName(); + + executor.execute(() -> + { + try + { + chatClient.submitPb(playerName, boss, pb); + } + catch (Exception ex) + { + log.warn("unable to submit personal best", ex); + } + finally + { + chatInput.resume(); + } + }); + + return true; + } + + private void gambleCountLookup(ChatMessage chatMessage, String message) + { + if (!config.gc()) + { + return; + } + + ChatMessageType type = chatMessage.getType(); + + final String player; + if (type == ChatMessageType.PRIVATECHATOUT) + { + player = client.getLocalPlayer().getName(); + } + else + { + player = Text.sanitize(chatMessage.getName()); + } + + int gc; + try + { + gc = chatClient.getGc(player); + } + catch (IOException ex) + { + log.debug("unable to lookup gamble count", ex); + return; + } + + String response = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Barbarian Assault High-level gambles: ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(gc)) + .build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + + private boolean gambleCountSubmit(ChatInput chatInput, String value) + { + final int gc = client.getVar(Varbits.BA_GC); + final String playerName = client.getLocalPlayer().getName(); + + executor.execute(() -> + { + try + { + chatClient.submitGc(playerName, gc); + } + catch (Exception ex) + { + log.warn("unable to submit gamble count", ex); + } + finally + { + chatInput.resume(); + } + }); + + return true; + } + + /** + * Looks up the item price and changes the original message to the + * response. + * + * @param chatMessage The chat message containing the command. + * @param message The chat message + */ + private void itemPriceLookup(ChatMessage chatMessage, String message) + { + if (!config.price()) + { + return; + } + + if (message.length() <= PRICE_COMMAND_STRING.length()) + { + return; + } + + MessageNode messageNode = chatMessage.getMessageNode(); + String search = message.substring(PRICE_COMMAND_STRING.length() + 1); + + List results = itemManager.search(search); + + if (!results.isEmpty()) + { + ItemPrice item = retrieveFromList(results, search); + + int itemId = item.getId(); + int itemPrice = item.getPrice(); + + final ChatMessageBuilder builder = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Price of ") + .append(ChatColorType.HIGHLIGHT) + .append(item.getName()) + .append(ChatColorType.NORMAL) + .append(": GE average ") + .append(ChatColorType.HIGHLIGHT) + .append(QuantityFormatter.formatNumber(itemPrice)); + + ItemComposition itemComposition = itemManager.getItemComposition(itemId); + final int alchPrice = itemComposition.getHaPrice(); + builder + .append(ChatColorType.NORMAL) + .append(" HA value ") + .append(ChatColorType.HIGHLIGHT) + .append(QuantityFormatter.formatNumber(alchPrice)); + + String response = builder.build(); + + log.debug("Setting response {}", response); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + } + + /** + * Looks up the player skill and changes the original message to the + * response. + * + * @param chatMessage The chat message containing the command. + * @param message The chat message + */ + @VisibleForTesting + void playerSkillLookup(ChatMessage chatMessage, String message) + { + if (!config.lvl()) + { + return; + } + + String search; + if (message.equalsIgnoreCase(TOTAL_LEVEL_COMMAND_STRING)) + { + search = "total"; + } + else + { + if (message.length() <= LEVEL_COMMAND_STRING.length()) + { + return; + } + + search = message.substring(LEVEL_COMMAND_STRING.length() + 1); + } + + search = SkillAbbreviations.getFullName(search); + final HiscoreSkill skill; + try + { + skill = HiscoreSkill.valueOf(search.toUpperCase()); + } + catch (IllegalArgumentException i) + { + return; + } + + final HiscoreLookup lookup = getCorrectLookupFor(chatMessage); + + try + { + final SingleHiscoreSkillResult result = hiscoreClient.lookup(lookup.getName(), skill, lookup.getEndpoint()); + + if (result == null) + { + log.warn("unable to look up skill {} for {}: not found", skill, search); + return; + } + + final Skill hiscoreSkill = result.getSkill(); + + ChatMessageBuilder chatMessageBuilder = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Level ") + .append(ChatColorType.HIGHLIGHT) + .append(skill.getName()).append(": ").append(String.valueOf(hiscoreSkill.getLevel())) + .append(ChatColorType.NORMAL); + if (hiscoreSkill.getExperience() != -1) + { + chatMessageBuilder.append(" Experience: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.format("%,d", hiscoreSkill.getExperience())) + .append(ChatColorType.NORMAL); + } + if (hiscoreSkill.getRank() != -1) + { + chatMessageBuilder.append(" Rank: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.format("%,d", hiscoreSkill.getRank())); + } + + final String response = chatMessageBuilder.build(); + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + catch (IOException ex) + { + log.warn("unable to look up skill {} for {}", skill, search, ex); + } + } + + private void combatLevelLookup(ChatMessage chatMessage, String message) + { + if (!config.lvl()) + { + return; + } + + ChatMessageType type = chatMessage.getType(); + + String player; + if (type == ChatMessageType.PRIVATECHATOUT) + { + player = client.getLocalPlayer().getName(); + } + else + { + player = Text.sanitize(chatMessage.getName()); + } + + try + { + HiscoreResult playerStats = hiscoreClient.lookup(player); + + if (playerStats == null) + { + log.warn("Error fetching hiscore data: not found"); + return; + } + + int attack = playerStats.getAttack().getLevel(); + int strength = playerStats.getStrength().getLevel(); + int defence = playerStats.getDefence().getLevel(); + int hitpoints = playerStats.getHitpoints().getLevel(); + int ranged = playerStats.getRanged().getLevel(); + int prayer = playerStats.getPrayer().getLevel(); + int magic = playerStats.getMagic().getLevel(); + int combatLevel = Experience.getCombatLevel(attack, strength, defence, hitpoints, magic, ranged, prayer); + + String response = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Combat Level: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(combatLevel)) + .append(ChatColorType.NORMAL) + .append(" A: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(attack)) + .append(ChatColorType.NORMAL) + .append(" S: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(strength)) + .append(ChatColorType.NORMAL) + .append(" D: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(defence)) + .append(ChatColorType.NORMAL) + .append(" H: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(hitpoints)) + .append(ChatColorType.NORMAL) + .append(" R: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(ranged)) + .append(ChatColorType.NORMAL) + .append(" P: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(prayer)) + .append(ChatColorType.NORMAL) + .append(" M: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(magic)) + .build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + catch (IOException ex) + { + log.warn("Error fetching hiscore data", ex); + } + } + + private void leaguePointsLookup(ChatMessage chatMessage, String message) + { + if (!config.lp()) + { + return; + } + + minigameLookup(chatMessage, HiscoreSkill.LEAGUE_POINTS); + } + + private void bountyHunterHunterLookup(ChatMessage chatMessage, String message) + { + if (!config.bh()) + { + return; + } + + minigameLookup(chatMessage, HiscoreSkill.BOUNTY_HUNTER_HUNTER); + } + + private void bountyHunterRogueLookup(ChatMessage chatMessage, String message) + { + if (!config.bhRogue()) + { + return; + } + + minigameLookup(chatMessage, HiscoreSkill.BOUNTY_HUNTER_ROGUE); + } + + private void lastManStandingLookup(ChatMessage chatMessage, String message) + { + if (!config.lms()) + { + return; + } + + minigameLookup(chatMessage, HiscoreSkill.LAST_MAN_STANDING); + } + + private void minigameLookup(ChatMessage chatMessage, HiscoreSkill minigame) + { + try + { + final Skill hiscoreSkill; + final HiscoreLookup lookup = getCorrectLookupFor(chatMessage); + + // League points only exist on the league hiscores + final HiscoreEndpoint endPoint = minigame == HiscoreSkill.LEAGUE_POINTS ? + HiscoreEndpoint.LEAGUE : + lookup.getEndpoint(); + + final HiscoreResult result = hiscoreClient.lookup(lookup.getName(), endPoint); + + if (result == null) + { + log.warn("error looking up {} score: not found", minigame.getName().toLowerCase()); + return; + } + + switch (minigame) + { + case BOUNTY_HUNTER_HUNTER: + hiscoreSkill = result.getBountyHunterHunter(); + break; + case BOUNTY_HUNTER_ROGUE: + hiscoreSkill = result.getBountyHunterRogue(); + break; + case LAST_MAN_STANDING: + hiscoreSkill = result.getLastManStanding(); + break; + case LEAGUE_POINTS: + hiscoreSkill = result.getLeaguePoints(); + break; + default: + log.warn("error looking up {} score: not implemented", minigame.getName().toLowerCase()); + return; + } + + int score = hiscoreSkill.getLevel(); + if (score == -1) + { + return; + } + + ChatMessageBuilder chatMessageBuilder = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append(minigame.getName()) + .append(" Score: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.format("%,d", score)); + + int rank = hiscoreSkill.getRank(); + if (rank != -1) + { + chatMessageBuilder.append(ChatColorType.NORMAL) + .append(" Rank: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.format("%,d", rank)); + } + + String response = chatMessageBuilder.build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + catch (IOException ex) + { + log.warn("error looking up {}", minigame.getName().toLowerCase(), ex); + } + } + + private void clueLookup(ChatMessage chatMessage, String message) + { + if (!config.clue()) + { + return; + } + + String search; + + if (message.equalsIgnoreCase(CLUES_COMMAND_STRING)) + { + search = "total"; + } + else + { + search = message.substring(CLUES_COMMAND_STRING.length() + 1); + } + + try + { + final Skill hiscoreSkill; + final HiscoreLookup lookup = getCorrectLookupFor(chatMessage); + final HiscoreResult result = hiscoreClient.lookup(lookup.getName(), lookup.getEndpoint()); + + if (result == null) + { + log.warn("error looking up clues: not found"); + return; + } + + String level = search.toLowerCase(); + + switch (level) + { + case "beginner": + hiscoreSkill = result.getClueScrollBeginner(); + break; + case "easy": + hiscoreSkill = result.getClueScrollEasy(); + break; + case "medium": + hiscoreSkill = result.getClueScrollMedium(); + break; + case "hard": + hiscoreSkill = result.getClueScrollHard(); + break; + case "elite": + hiscoreSkill = result.getClueScrollElite(); + break; + case "master": + hiscoreSkill = result.getClueScrollMaster(); + break; + case "total": + hiscoreSkill = result.getClueScrollAll(); + break; + default: + return; + } + + int quantity = hiscoreSkill.getLevel(); + int rank = hiscoreSkill.getRank(); + if (quantity == -1) + { + return; + } + + ChatMessageBuilder chatMessageBuilder = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Clue scroll (" + level + ")").append(": ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(quantity)); + + if (rank != -1) + { + chatMessageBuilder.append(ChatColorType.NORMAL) + .append(" Rank: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.format("%,d", rank)); + } + + String response = chatMessageBuilder.build(); + + log.debug("Setting response {}", response); + final MessageNode messageNode = chatMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + catch (IOException ex) + { + log.warn("error looking up clues", ex); + } + } + + /** + * Gets correct lookup data for message + * + * @param chatMessage chat message + * @return hiscore lookup data + */ + private HiscoreLookup getCorrectLookupFor(final ChatMessage chatMessage) + { + Player localPlayer = client.getLocalPlayer(); + final String player = Text.sanitize(chatMessage.getName()); + + // If we are sending the message then just use the local hiscore endpoint for the world + if (chatMessage.getType().equals(ChatMessageType.PRIVATECHATOUT) + || player.equals(localPlayer.getName())) + { + return new HiscoreLookup(localPlayer.getName(), hiscoreEndpoint); + } + + // Public chat on a leagues world is always league hiscores, regardless of icon + if (chatMessage.getType() == ChatMessageType.PUBLICCHAT || chatMessage.getType() == ChatMessageType.MODCHAT) + { + if (client.getWorldType().contains(WorldType.LEAGUE)) + { + return new HiscoreLookup(player, HiscoreEndpoint.LEAGUE); + } + } + + // Get ironman status from their icon in chat, this handles leagues too + HiscoreEndpoint endpoint = getHiscoreEndpointByName(chatMessage.getName()); + return new HiscoreLookup(player, endpoint); + } + + /** + * Compares the names of the items in the list with the original input. + * Returns the item if its name is equal to the original input or the + * shortest match if no exact match is found. + * + * @param items List of items. + * @param originalInput String with the original input. + * @return Item which has a name equal to the original input. + */ + private ItemPrice retrieveFromList(List items, String originalInput) + { + ItemPrice shortest = null; + for (ItemPrice item : items) + { + if (item.getName().toLowerCase().equals(originalInput.toLowerCase())) + { + return item; + } + + if (shortest == null || item.getName().length() < shortest.getName().length()) + { + shortest = item; + } + } + + // Take a guess + return shortest; + } + + /** + * Looks up the ironman status of the local player. Does NOT work on other players. + * + * @return hiscore endpoint + */ + private HiscoreEndpoint getLocalHiscoreEndpointType() + { + EnumSet worldType = client.getWorldType(); + if (worldType.contains(WorldType.LEAGUE)) + { + return HiscoreEndpoint.LEAGUE; + } + + return toEndPoint(client.getAccountType()); + } + + /** + * Returns the ironman status based on the symbol in the name of the player. + * + * @param name player name + * @return hiscore endpoint + */ + private static HiscoreEndpoint getHiscoreEndpointByName(final String name) + { + if (name.contains(IconID.IRONMAN.toString())) + { + return HiscoreEndpoint.IRONMAN; + } + else if (name.contains(IconID.ULTIMATE_IRONMAN.toString())) + { + return HiscoreEndpoint.ULTIMATE_IRONMAN; + } + else if (name.contains(IconID.HARDCORE_IRONMAN.toString())) + { + return HiscoreEndpoint.HARDCORE_IRONMAN; + } + else if (name.contains(IconID.LEAGUE.toString())) + { + return HiscoreEndpoint.LEAGUE; + } + else + { + return HiscoreEndpoint.NORMAL; + } + } + + /** + * Converts account type to hiscore endpoint + * + * @param accountType account type + * @return hiscore endpoint + */ + private static HiscoreEndpoint toEndPoint(final AccountType accountType) + { + switch (accountType) + { + case IRONMAN: + return HiscoreEndpoint.IRONMAN; + case ULTIMATE_IRONMAN: + return HiscoreEndpoint.ULTIMATE_IRONMAN; + case HARDCORE_IRONMAN: + return HiscoreEndpoint.HARDCORE_IRONMAN; + default: + return HiscoreEndpoint.NORMAL; + } + } + + @Value + private static class HiscoreLookup + { + private final String name; + private final HiscoreEndpoint endpoint; + } + + private static String longBossName(String boss) + { + switch (boss.toLowerCase()) + { + case "corp": + return "Corporeal Beast"; + + case "jad": + case "tzhaar fight cave": + return "TzTok-Jad"; + + case "kq": + return "Kalphite Queen"; + + case "chaos ele": + return "Chaos Elemental"; + + case "dusk": + case "dawn": + case "gargs": + return "Grotesque Guardians"; + + case "crazy arch": + return "Crazy Archaeologist"; + + case "deranged arch": + return "Deranged Archaeologist"; + + case "mole": + return "Giant Mole"; + + case "vetion": + return "Vet'ion"; + + case "vene": + return "Venenatis"; + + case "kbd": + return "King Black Dragon"; + + case "vork": + return "Vorkath"; + + case "sire": + return "Abyssal Sire"; + + case "smoke devil": + case "thermy": + return "Thermonuclear Smoke Devil"; + + case "cerb": + return "Cerberus"; + + case "zuk": + case "inferno": + return "TzKal-Zuk"; + + case "hydra": + return "Alchemical Hydra"; + + // gwd + case "sara": + case "saradomin": + case "zilyana": + case "zily": + return "Commander Zilyana"; + case "zammy": + case "zamorak": + case "kril": + case "kril trutsaroth": + return "K'ril Tsutsaroth"; + case "arma": + case "kree": + case "kreearra": + case "armadyl": + return "Kree'arra"; + case "bando": + case "bandos": + case "graardor": + return "General Graardor"; + + // dks + case "supreme": + return "Dagannoth Supreme"; + case "rex": + return "Dagannoth Rex"; + case "prime": + return "Dagannoth Prime"; + + case "wt": + return "Wintertodt"; + case "barrows": + return "Barrows Chests"; + case "herbi": + return "Herbiboar"; + + // cox + case "cox": + case "xeric": + case "chambers": + case "olm": + case "raids": + return "Chambers of Xeric"; + + // cox cm + case "cox cm": + case "xeric cm": + case "chambers cm": + case "olm cm": + case "raids cm": + case "chambers of xeric - challenge mode": + return "Chambers of Xeric Challenge Mode"; + + // tob + case "tob": + case "theatre": + case "verzik": + case "verzik vitur": + case "raids 2": + return "Theatre of Blood"; + + // agility course + case "prif": + case "prifddinas": + return "Prifddinas Agility Course"; + + // The Gauntlet + case "gaunt": + case "gauntlet": + case "the gauntlet": + return "Gauntlet"; + + // Corrupted Gauntlet + case "cgaunt": + case "cgauntlet": + case "the corrupted gauntlet": + return "Corrupted Gauntlet"; + + case "nm": + case "tnm": + case "nmare": + case "the nightmare": + return "Nightmare"; + + // Hallowed Sepulchre + case "hs": + case "sepulchre": + case "ghc": + return "Hallowed Sepulchre"; + case "hs1": + case "hs 1": + return "Hallowed Sepulchre Floor 1"; + case "hs2": + case "hs 2": + return "Hallowed Sepulchre Floor 2"; + case "hs3": + case "hs 3": + return "Hallowed Sepulchre Floor 3"; + case "hs4": + case "hs 4": + return "Hallowed Sepulchre Floor 4"; + case "hs5": + case "hs 5": + return "Hallowed Sepulchre Floor 5"; + + // Ape Atoll Agility + case "aa": + case "ape atoll": + return "Ape Atoll Agility"; + + // Draynor Village Rooftop Course + case "draynor": + case "draynor agility": + return "Draynor Village Rooftop"; + + // Al-Kharid Rooftop Course + case "al kharid": + case "al kharid agility": + case "al-kharid": + case "al-kharid agility": + case "alkharid": + case "alkharid agility": + return "Al-Kharid Rooftop"; + + // Varrock Rooftop Course + case "varrock": + case "varrock agility": + return "Varrock Rooftop"; + + // Canifis Rooftop Course + case "canifis": + case "canifis agility": + return "Canifis Rooftop"; + + // Falador Rooftop Course + case "fally": + case "fally agility": + case "falador": + case "falador agility": + return "Falador Rooftop"; + + // Seers' Village Rooftop Course + case "seers": + case "seers agility": + case "seers village": + case "seers village agility": + case "seers'": + case "seers' agility": + case "seers' village": + case "seers' village agility": + case "seer's": + case "seer's agility": + case "seer's village": + case "seer's village agility": + return "Seers' Village Rooftop"; + + // Pollnivneach Rooftop Course + case "pollnivneach": + case "pollnivneach agility": + return "Pollnivneach Rooftop"; + + // Rellekka Rooftop Course + case "rellekka": + case "rellekka agility": + return "Rellekka Rooftop"; + + // Ardougne Rooftop Course + case "ardy": + case "ardy agility": + case "ardy rooftop": + case "ardougne": + case "ardougne agility": + return "Ardougne Rooftop"; + + // Agility Pyramid + case "ap": + case "pyramid": + return "Agility Pyramid"; + + // Barbarian Outpost + case "barb": + case "barb outpost": + return "Barbarian Outpost"; + + // Brimhaven Agility Arena + case "brimhaven": + case "brimhaven agility": + return "Agility Arena"; + + // Dorgesh-Kaan Agility Course + case "dorg": + case "dorgesh kaan": + case "dorgesh-kaan": + return "Dorgesh-Kaan Agility"; + + // Gnome Stronghold Agility Course + case "gnome stronghold": + return "Gnome Stronghold Agility"; + + // Penguin Agility + case "penguin": + return "Penguin Agility"; + + // Werewolf Agility + case "werewolf": + return "Werewolf Agility"; + + // Skullball + case "skullball": + return "Werewolf Skullball"; + + // Wilderness Agility Course + case "wildy": + case "wildy agility": + return "Wilderness Agility"; + + default: + return WordUtils.capitalize(boss); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatKeyboardListener.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatKeyboardListener.java new file mode 100644 index 0000000000..69456fd861 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatKeyboardListener.java @@ -0,0 +1,107 @@ +/* + * 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.chatcommands; + +import java.awt.event.KeyEvent; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.Client; +import net.runelite.api.ScriptID; +import net.runelite.api.VarClientInt; +import net.runelite.api.VarClientStr; +import net.runelite.api.vars.InputType; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.input.KeyListener; + +@Singleton +public class ChatKeyboardListener implements KeyListener +{ + @Inject + private ChatCommandsConfig chatCommandsConfig; + + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Override + public void keyTyped(KeyEvent e) + { + + } + + @Override + public void keyPressed(KeyEvent e) + { + if (chatCommandsConfig.clearSingleWord().matches(e)) + { + int inputTye = client.getVar(VarClientInt.INPUT_TYPE); + String input = inputTye == InputType.NONE.getType() + ? client.getVar(VarClientStr.CHATBOX_TYPED_TEXT) + : client.getVar(VarClientStr.INPUT_TEXT); + + if (input != null) + { + // remove trailing space + while (input.endsWith(" ")) + { + input = input.substring(0, input.length() - 1); + } + + // find next word + int idx = input.lastIndexOf(' ') + 1; + final String replacement = input.substring(0, idx); + + clientThread.invoke(() -> applyText(inputTye, replacement)); + } + } + else if (chatCommandsConfig.clearChatBox().matches(e)) + { + int inputTye = client.getVar(VarClientInt.INPUT_TYPE); + clientThread.invoke(() -> applyText(inputTye, "")); + } + } + + private void applyText(int inputType, String replacement) + { + if (inputType == InputType.NONE.getType()) + { + client.setVar(VarClientStr.CHATBOX_TYPED_TEXT, replacement); + client.runScript(ScriptID.CHAT_PROMPT_INIT); + } + else if (inputType == InputType.PRIVATE_MESSAGE.getType()) + { + client.setVar(VarClientStr.INPUT_TEXT, replacement); + client.runScript(ScriptID.CHAT_TEXT_INPUT_REBUILD, ""); + } + } + + @Override + public void keyReleased(KeyEvent e) + { + + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/SkillAbbreviations.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/SkillAbbreviations.java new file mode 100644 index 0000000000..754c2f869e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/SkillAbbreviations.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2017, 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.chatcommands; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import net.runelite.api.Skill; + +class SkillAbbreviations +{ + private static final Map MAP; + + static + { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + builder.put("ATK", Skill.ATTACK.getName()); + builder.put("ATT", Skill.ATTACK.getName()); + builder.put("DEF", Skill.DEFENCE.getName()); + builder.put("STR", Skill.STRENGTH.getName()); + builder.put("HEALTH", Skill.HITPOINTS.getName()); + builder.put("HIT", Skill.HITPOINTS.getName()); + builder.put("HITPOINT", Skill.HITPOINTS.getName()); + builder.put("HP", Skill.HITPOINTS.getName()); + builder.put("RANGE", Skill.RANGED.getName()); + builder.put("RANGING", Skill.RANGED.getName()); + builder.put("RNG", Skill.RANGED.getName()); + builder.put("PRAY", Skill.PRAYER.getName()); + builder.put("MAG", Skill.MAGIC.getName()); + builder.put("MAGE", Skill.MAGIC.getName()); + builder.put("COOK", Skill.COOKING.getName()); + builder.put("WC", Skill.WOODCUTTING.getName()); + builder.put("WOOD", Skill.WOODCUTTING.getName()); + builder.put("WOODCUT", Skill.WOODCUTTING.getName()); + builder.put("FLETCH", Skill.FLETCHING.getName()); + builder.put("FISH", Skill.FISHING.getName()); + builder.put("FM", Skill.FIREMAKING.getName()); + builder.put("FIRE", Skill.FIREMAKING.getName()); + builder.put("CRAFT", Skill.CRAFTING.getName()); + builder.put("SMITH", Skill.SMITHING.getName()); + builder.put("MINE", Skill.MINING.getName()); + builder.put("HL", Skill.HERBLORE.getName()); + builder.put("HERB", Skill.HERBLORE.getName()); + builder.put("AGI", Skill.AGILITY.getName()); + builder.put("AGIL", Skill.AGILITY.getName()); + builder.put("THIEF", Skill.THIEVING.getName()); + builder.put("SLAY", Skill.SLAYER.getName()); + builder.put("FARM", Skill.FARMING.getName()); + builder.put("RC", Skill.RUNECRAFT.getName()); + builder.put("RUNE", Skill.RUNECRAFT.getName()); + builder.put("RUNECRAFTING", Skill.RUNECRAFT.getName()); + builder.put("HUNT", Skill.HUNTER.getName()); + builder.put("CON", Skill.CONSTRUCTION.getName()); + builder.put("CONSTRUCT", Skill.CONSTRUCTION.getName()); + builder.put("ALL", Skill.OVERALL.getName()); + builder.put("TOTAL", Skill.OVERALL.getName()); + MAP = builder.build(); + } + + /** + * Takes a string representing the name of a skill, and if abbreviated, + * expands it into its full canonical name. Case-insensitive. + * + * @param abbrev Skill name that may be abbreviated. + * @return Full skill name if recognized, else the original string. + */ + static String getFullName(String abbrev) + { + return MAP.getOrDefault(abbrev.toUpperCase(), abbrev); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java new file mode 100644 index 0000000000..2b9a45c2e9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2018, Magic fTail + * Copyright (c) 2019, osrs-music-map + * 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.chatfilter; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; + +@ConfigGroup("chatfilter") +public interface ChatFilterConfig extends Config +{ + @ConfigSection( + name = "Filter Lists", + description = "Custom Word, Regex, and Username filter lists", + position = 0, + closedByDefault = true + ) + String filterLists = "filterLists"; + + @ConfigItem( + keyName = "filteredWords", + name = "Filtered Words", + description = "List of filtered words, separated by commas", + position = 1, + section = filterLists + ) + default String filteredWords() + { + return ""; + } + + @ConfigItem( + keyName = "filteredRegex", + name = "Filtered Regex", + description = "List of regular expressions to filter, one per line", + position = 2, + section = filterLists + ) + default String filteredRegex() + { + return ""; + } + + @ConfigItem( + keyName = "filteredNames", + name = "Filtered Names", + description = "List of filtered names, one per line. Accepts regular expressions", + position = 3, + section = filterLists + ) + default String filteredNames() + { + return ""; + } + + @ConfigItem( + keyName = "filterType", + name = "Filter type", + description = "Configures how the messages are filtered", + position = 4 + ) + default ChatFilterType filterType() + { + return ChatFilterType.CENSOR_WORDS; + } + + @ConfigItem( + keyName = "filterFriends", + name = "Filter Friends", + description = "Filter your friends' messages", + position = 5 + ) + default boolean filterFriends() + { + return false; + } + + @ConfigItem( + keyName = "filterClan", + name = "Filter Friends Chat Members", + description = "Filter your friends chat members' messages", + position = 6 + ) + default boolean filterFriendsChat() + { + return false; + } + + @ConfigItem( + keyName = "filterLogin", + name = "Filter Logged In/Out Messages", + description = "Filter your private chat to remove logged in/out messages", + position = 7 + ) + default boolean filterLogin() + { + return false; + } + + @ConfigItem( + keyName = "filterGameChat", + name = "Filter Game Chat", + description = "Filter your game chat messages", + position = 8 + ) + default boolean filterGameChat() + { + return false; + } + + @ConfigItem( + keyName = "collapseGameChat", + name = "Collapse Game Chat", + description = "Collapse duplicate game chat messages into a single line", + position = 9 + ) + default boolean collapseGameChat() + { + return false; + } + + @ConfigItem( + keyName = "collapsePlayerChat", + name = "Collapse Player Chat", + description = "Collapse duplicate player chat messages into a single line", + position = 10 + ) + default boolean collapsePlayerChat() + { + return false; + } + + @ConfigItem( + keyName = "maxRepeatedPublicChats", + name = "Max repeated public chats", + description = "Block player chat message if repeated this many times. 0 is off", + position = 11 + ) + default int maxRepeatedPublicChats() + { + return 0; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java new file mode 100644 index 0000000000..5340be1df0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2018, Magic fTail + * Copyright (c) 2019, osrs-music-map + * 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.chatfilter; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Provides; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.inject.Inject; +import net.runelite.api.ChatMessageType; +import static net.runelite.api.ChatMessageType.ENGINE; +import static net.runelite.api.ChatMessageType.GAMEMESSAGE; +import static net.runelite.api.ChatMessageType.ITEM_EXAMINE; +import static net.runelite.api.ChatMessageType.MODCHAT; +import static net.runelite.api.ChatMessageType.NPC_EXAMINE; +import static net.runelite.api.ChatMessageType.OBJECT_EXAMINE; +import static net.runelite.api.ChatMessageType.PUBLICCHAT; +import static net.runelite.api.ChatMessageType.SPAM; +import net.runelite.api.Client; +import net.runelite.api.MessageNode; +import net.runelite.api.Player; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.OverheadTextChanged; +import net.runelite.api.events.ScriptCallbackEvent; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.game.FriendChatManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.Text; +import org.apache.commons.lang3.StringUtils; + +@PluginDescriptor( + name = "Chat Filter", + description = "Censor user configurable words or patterns from chat", + enabledByDefault = false +) +public class ChatFilterPlugin extends Plugin +{ + private static final Splitter NEWLINE_SPLITTER = Splitter + .on("\n") + .omitEmptyStrings() + .trimResults(); + + @VisibleForTesting + static final String CENSOR_MESSAGE = "Hey, everyone, I just tried to say something very silly!"; + + private static final Set COLLAPSIBLE_MESSAGETYPES = ImmutableSet.of( + ENGINE, + GAMEMESSAGE, + ITEM_EXAMINE, + NPC_EXAMINE, + OBJECT_EXAMINE, + SPAM, + PUBLICCHAT, + MODCHAT + ); + + private final CharMatcher jagexPrintableCharMatcher = Text.JAGEX_PRINTABLE_CHAR_MATCHER; + private final List filteredPatterns = new ArrayList<>(); + private final List filteredNamePatterns = new ArrayList<>(); + + private static class Duplicate + { + int messageId; + int count; + } + + private final LinkedHashMap duplicateChatCache = new LinkedHashMap() + { + private static final int MAX_ENTRIES = 100; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) + { + return size() > MAX_ENTRIES; + } + }; + + @Inject + private Client client; + + @Inject + private ChatFilterConfig config; + + @Inject + private FriendChatManager friendChatManager; + + @Provides + ChatFilterConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(ChatFilterConfig.class); + } + + @Override + protected void startUp() throws Exception + { + updateFilteredPatterns(); + client.refreshChat(); + } + + @Override + protected void shutDown() throws Exception + { + filteredPatterns.clear(); + duplicateChatCache.clear(); + client.refreshChat(); + } + + @Subscribe + public void onScriptCallbackEvent(ScriptCallbackEvent event) + { + if (!"chatFilterCheck".equals(event.getEventName())) + { + return; + } + + int[] intStack = client.getIntStack(); + int intStackSize = client.getIntStackSize(); + String[] stringStack = client.getStringStack(); + int stringStackSize = client.getStringStackSize(); + + final int messageType = intStack[intStackSize - 2]; + final int messageId = intStack[intStackSize - 1]; + String message = stringStack[stringStackSize - 1]; + + ChatMessageType chatMessageType = ChatMessageType.of(messageType); + final MessageNode messageNode = client.getMessages().get(messageId); + final String name = messageNode.getName(); + int duplicateCount = 0; + boolean blockMessage = false; + + // Only filter public chat and private messages + switch (chatMessageType) + { + case PUBLICCHAT: + case MODCHAT: + case AUTOTYPER: + case PRIVATECHAT: + case MODPRIVATECHAT: + case FRIENDSCHAT: + if (shouldFilterPlayerMessage(Text.removeTags(name))) + { + message = censorMessage(name, message); + blockMessage = message == null; + } + break; + case GAMEMESSAGE: + case ENGINE: + case ITEM_EXAMINE: + case NPC_EXAMINE: + case OBJECT_EXAMINE: + case SPAM: + if (config.filterGameChat()) + { + message = censorMessage(null, message); + blockMessage = message == null; + } + break; + case LOGINLOGOUTNOTIFICATION: + if (config.filterLogin()) + { + blockMessage = true; + } + break; + } + + boolean shouldCollapse = chatMessageType == PUBLICCHAT || chatMessageType == MODCHAT + ? config.collapsePlayerChat() + : COLLAPSIBLE_MESSAGETYPES.contains(chatMessageType) && config.collapseGameChat(); + if (!blockMessage && shouldCollapse) + { + Duplicate duplicateCacheEntry = duplicateChatCache.get(name + ":" + message); + if (duplicateCacheEntry != null) + { + blockMessage = duplicateCacheEntry.messageId != messageId || + ((chatMessageType == PUBLICCHAT || chatMessageType == MODCHAT) && + config.maxRepeatedPublicChats() > 0 && duplicateCacheEntry.count > config.maxRepeatedPublicChats()); + duplicateCount = duplicateCacheEntry.count; + } + } + + if (blockMessage) + { + // Block the message + intStack[intStackSize - 3] = 0; + } + else + { + // Replace the message + if (duplicateCount > 1) + { + message += " (" + duplicateCount + ")"; + } + + stringStack[stringStackSize - 1] = message; + } + } + + @Subscribe + public void onOverheadTextChanged(OverheadTextChanged event) + { + if (!(event.getActor() instanceof Player) || !shouldFilterPlayerMessage(event.getActor().getName())) + { + return; + } + + String message = censorMessage(event.getActor().getName(), event.getOverheadText()); + + if (message == null) + { + message = " "; + } + + event.getActor().setOverheadText(message); + } + + @Subscribe(priority = -2) // run after ChatMessageManager + public void onChatMessage(ChatMessage chatMessage) + { + if (COLLAPSIBLE_MESSAGETYPES.contains(chatMessage.getType())) + { + final MessageNode messageNode = chatMessage.getMessageNode(); + // remove and re-insert into map to move to end of list + final String key = messageNode.getName() + ":" + messageNode.getValue(); + Duplicate duplicate = duplicateChatCache.remove(key); + if (duplicate == null) + { + duplicate = new Duplicate(); + } + + duplicate.count++; + duplicate.messageId = messageNode.getId(); + duplicateChatCache.put(key, duplicate); + } + } + + boolean shouldFilterPlayerMessage(String playerName) + { + boolean isMessageFromSelf = playerName.equals(client.getLocalPlayer().getName()); + return !isMessageFromSelf && + (config.filterFriends() || !client.isFriended(playerName, false)) && + (config.filterFriendsChat() || !friendChatManager.isMember(playerName)); + } + + String censorMessage(final String username, final String message) + { + String strippedMessage = jagexPrintableCharMatcher.retainFrom(message) + .replace('\u00A0', ' '); + if (username != null && shouldFilterByName(username)) + { + switch (config.filterType()) + { + case CENSOR_WORDS: + return StringUtils.repeat('*', strippedMessage.length()); + case CENSOR_MESSAGE: + return CENSOR_MESSAGE; + case REMOVE_MESSAGE: + return null; + } + } + + boolean filtered = false; + for (Pattern pattern : filteredPatterns) + { + Matcher m = pattern.matcher(strippedMessage); + + StringBuffer sb = new StringBuffer(); + + while (m.find()) + { + switch (config.filterType()) + { + case CENSOR_WORDS: + m.appendReplacement(sb, StringUtils.repeat('*', m.group(0).length())); + filtered = true; + break; + case CENSOR_MESSAGE: + return CENSOR_MESSAGE; + case REMOVE_MESSAGE: + return null; + } + } + m.appendTail(sb); + + strippedMessage = sb.toString(); + } + + return filtered ? strippedMessage : message; + } + + void updateFilteredPatterns() + { + filteredPatterns.clear(); + filteredNamePatterns.clear(); + + Text.fromCSV(config.filteredWords()).stream() + .map(s -> Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE)) + .forEach(filteredPatterns::add); + + NEWLINE_SPLITTER.splitToList(config.filteredRegex()).stream() + .map(ChatFilterPlugin::compilePattern) + .filter(Objects::nonNull) + .forEach(filteredPatterns::add); + + NEWLINE_SPLITTER.splitToList(config.filteredNames()).stream() + .map(ChatFilterPlugin::compilePattern) + .filter(Objects::nonNull) + .forEach(filteredNamePatterns::add); + } + + private static Pattern compilePattern(String pattern) + { + try + { + return Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + } + catch (PatternSyntaxException ex) + { + return null; + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!"chatfilter".equals(event.getGroup())) + { + return; + } + + updateFilteredPatterns(); + + //Refresh chat after config change to reflect current rules + client.refreshChat(); + } + + @VisibleForTesting + boolean shouldFilterByName(final String playerName) + { + String sanitizedName = Text.standardize(playerName); + for (Pattern pattern : filteredNamePatterns) + { + Matcher m = pattern.matcher(sanitizedName); + if (m.find()) + { + return true; + } + } + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java new file mode 100644 index 0000000000..f0aef43471 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018, Magic fTail + * 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.chatfilter; + +public enum ChatFilterType +{ + CENSOR_WORDS, + CENSOR_MESSAGE, + REMOVE_MESSAGE +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryConfig.java new file mode 100644 index 0000000000..a552d39951 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryConfig.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018, TheStonedTurtle + * 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.chathistory; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("chathistory") +public interface ChatHistoryConfig extends Config +{ + @ConfigItem( + keyName = "retainChatHistory", + name = "Retain Chat History", + description = "Retains chat history when logging in/out or world hopping", + position = 0 + ) + default boolean retainChatHistory() + { + return true; + } + + @ConfigItem( + keyName = "pmTargetCycling", + name = "PM Target Cycling", + description = "Pressing Tab while sending a PM will cycle the target username based on PM history", + position = 1 + ) + default boolean pmTargetCycling() + { + return true; + } + + @ConfigItem( + keyName = "copyToClipboard", + name = "Copy to clipboard", + description = "Add option on chat messages to copy them to clipboard", + position = 2 + ) + default boolean copyToClipboard() + { + return true; + } + + @ConfigItem( + keyName = "clearHistory", + name = "Clear history option for all tabs", + description = "Add 'Clear history' option chatbox tab buttons", + position = 3 + ) + default boolean clearHistory() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryPlugin.java new file mode 100644 index 0000000000..8305c40bde --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatHistoryPlugin.java @@ -0,0 +1,425 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * Copyright (c) 2020, Anthony + * 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.chathistory; + +import com.google.common.base.Strings; +import com.google.common.collect.EvictingQueue; +import com.google.inject.Provides; +import java.awt.Color; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.KeyEvent; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.Queue; +import javax.inject.Inject; +import net.runelite.api.ChatLineBuffer; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.MessageNode; +import net.runelite.api.ScriptID; +import net.runelite.api.VarClientInt; +import net.runelite.api.VarClientStr; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.events.MenuOpened; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.vars.InputType; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import static net.runelite.api.widgets.WidgetInfo.TO_CHILD; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.input.KeyListener; +import net.runelite.client.input.KeyManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.Text; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +@PluginDescriptor( + name = "Chat History", + description = "Retain your chat history when logging in/out or world hopping", + tags = {"chat", "history", "retain", "cycle", "pm"} +) +public class ChatHistoryPlugin extends Plugin implements KeyListener +{ + private static final String WELCOME_MESSAGE = "Welcome to Old School RuneScape"; + private static final String CLEAR_HISTORY = "Clear history"; + private static final String COPY_TO_CLIPBOARD = "Copy to clipboard"; + private static final int CYCLE_HOTKEY = KeyEvent.VK_TAB; + private static final int FRIENDS_MAX_SIZE = 5; + + private Queue messageQueue; + private Deque friends; + + private String currentMessage = null; + + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Inject + private ChatHistoryConfig config; + + @Inject + private KeyManager keyManager; + + @Inject + private ChatMessageManager chatMessageManager; + + @Provides + ChatHistoryConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(ChatHistoryConfig.class); + } + + @Override + protected void startUp() + { + messageQueue = EvictingQueue.create(100); + friends = new ArrayDeque<>(FRIENDS_MAX_SIZE + 1); + keyManager.registerKeyListener(this); + } + + @Override + protected void shutDown() + { + messageQueue.clear(); + messageQueue = null; + friends.clear(); + friends = null; + currentMessage = null; + keyManager.unregisterKeyListener(this); + } + + @Subscribe + public void onChatMessage(ChatMessage chatMessage) + { + // Start sending old messages right after the welcome message, as that is most reliable source + // of information that chat history was reset + ChatMessageType chatMessageType = chatMessage.getType(); + if (chatMessageType == ChatMessageType.WELCOME && StringUtils.startsWithIgnoreCase(chatMessage.getMessage(), WELCOME_MESSAGE)) + { + if (!config.retainChatHistory()) + { + return; + } + + QueuedMessage queuedMessage; + + while ((queuedMessage = messageQueue.poll()) != null) + { + chatMessageManager.queue(queuedMessage); + } + + return; + } + + switch (chatMessageType) + { + case PRIVATECHATOUT: + case PRIVATECHAT: + case MODPRIVATECHAT: + final String name = Text.removeTags(chatMessage.getName()); + // Remove to ensure uniqueness & its place in history + if (!friends.remove(name)) + { + // If the friend didn't previously exist ensure deque capacity doesn't increase by adding them + if (friends.size() >= FRIENDS_MAX_SIZE) + { + friends.remove(); + } + } + friends.add(name); + // intentional fall-through + case PUBLICCHAT: + case MODCHAT: + case FRIENDSCHAT: + case CONSOLE: + final QueuedMessage queuedMessage = QueuedMessage.builder() + .type(chatMessageType) + .name(chatMessage.getName()) + .sender(chatMessage.getSender()) + .value(nbsp(chatMessage.getMessage())) + .runeLiteFormattedMessage(nbsp(chatMessage.getMessageNode().getRuneLiteFormatMessage())) + .timestamp(chatMessage.getTimestamp()) + .build(); + + if (!messageQueue.contains(queuedMessage)) + { + messageQueue.offer(queuedMessage); + } + } + } + + @Subscribe + public void onMenuOpened(MenuOpened event) + { + if (event.getMenuEntries().length < 2 || !config.copyToClipboard()) + { + return; + } + + // Use second entry as first one can be walk here with transparent chatbox + final MenuEntry entry = event.getMenuEntries()[event.getMenuEntries().length - 2]; + + if (entry.getType() != MenuAction.CC_OP_LOW_PRIORITY.getId() && entry.getType() != MenuAction.RUNELITE.getId()) + { + return; + } + + final int groupId = TO_GROUP(entry.getParam1()); + final int childId = TO_CHILD(entry.getParam1()); + + if (groupId != WidgetInfo.CHATBOX.getGroupId()) + { + return; + } + + final Widget widget = client.getWidget(groupId, childId); + final Widget parent = widget.getParent(); + + if (WidgetInfo.CHATBOX_MESSAGE_LINES.getId() != parent.getId()) + { + return; + } + + // Get child id of first chat message static child so we can substract this offset to link to dynamic child + // later + final int first = WidgetInfo.CHATBOX_FIRST_MESSAGE.getChildId(); + + // Convert current message static widget id to dynamic widget id of message node with message contents + // When message is right clicked, we are actually right clicking static widget that contains only sender. + // The actual message contents are stored in dynamic widgets that follow same order as static widgets. + // Every first dynamic widget is message sender and every second one is message contents. + final int dynamicChildId = (childId - first) * 2 + 1; + + // Extract and store message contents when menu is opened because dynamic children can change while right click + // menu is open and dynamicChildId will be outdated + final Widget messageContents = parent.getChild(dynamicChildId); + if (messageContents == null) + { + return; + } + + currentMessage = messageContents.getText(); + + final MenuEntry menuEntry = new MenuEntry(); + menuEntry.setOption(COPY_TO_CLIPBOARD); + menuEntry.setTarget(entry.getTarget()); + menuEntry.setType(MenuAction.RUNELITE.getId()); + menuEntry.setParam0(entry.getParam0()); + menuEntry.setParam1(entry.getParam1()); + menuEntry.setIdentifier(entry.getIdentifier()); + client.setMenuEntries(ArrayUtils.insert(1, client.getMenuEntries(), menuEntry)); + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked event) + { + final String menuOption = event.getMenuOption(); + + // The menu option for clear history is "Public: Clear history" + if (menuOption.endsWith(CLEAR_HISTORY)) + { + clearChatboxHistory(ChatboxTab.of(event.getWidgetId())); + } + else if (COPY_TO_CLIPBOARD.equals(menuOption) && !Strings.isNullOrEmpty(currentMessage)) + { + final StringSelection stringSelection = new StringSelection(Text.removeTags(currentMessage)); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); + } + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded entry) + { + final ChatboxTab tab = ChatboxTab.of(entry.getActionParam1()); + + if (tab == null || !config.clearHistory() || !Text.removeTags(entry.getOption()).equals(tab.getAfter())) + { + return; + } + + final MenuEntry clearEntry = new MenuEntry(); + clearEntry.setTarget(""); + clearEntry.setType(MenuAction.RUNELITE.getId()); + clearEntry.setParam0(entry.getActionParam0()); + clearEntry.setParam1(entry.getActionParam1()); + + if (tab == ChatboxTab.GAME) + { + // keep type as the original CC_OP to correctly group "Game: Clear history" with + // other tab "Game: *" options. + clearEntry.setType(entry.getType()); + } + + final StringBuilder messageBuilder = new StringBuilder(); + + if (tab != ChatboxTab.ALL) + { + messageBuilder.append(ColorUtil.wrapWithColorTag(tab.getName() + ": ", Color.YELLOW)); + } + + messageBuilder.append(CLEAR_HISTORY); + clearEntry.setOption(messageBuilder.toString()); + + final MenuEntry[] menuEntries = client.getMenuEntries(); + client.setMenuEntries(ArrayUtils.insert(menuEntries.length - 1, menuEntries, clearEntry)); + } + + private void clearMessageQueue(ChatboxTab tab) + { + if (tab == ChatboxTab.ALL || tab == ChatboxTab.PRIVATE) + { + friends.clear(); + } + + messageQueue.removeIf(e -> ArrayUtils.contains(tab.getMessageTypes(), e.getType())); + } + + private void clearChatboxHistory(ChatboxTab tab) + { + if (tab == null) + { + return; + } + + boolean removed = false; + for (ChatMessageType msgType : tab.getMessageTypes()) + { + final ChatLineBuffer lineBuffer = client.getChatLineMap().get(msgType.getType()); + if (lineBuffer == null) + { + continue; + } + + final MessageNode[] lines = lineBuffer.getLines().clone(); + for (final MessageNode line : lines) + { + if (line != null) + { + lineBuffer.removeMessageNode(line); + removed = true; + } + } + } + + if (removed) + { + clientThread.invoke(() -> client.runScript(ScriptID.BUILD_CHATBOX)); + } + + clearMessageQueue(tab); + } + + /** + * Small hack to prevent plugins checking for specific messages to match + * @param message message + * @return message with nbsp + */ + private static String nbsp(final String message) + { + if (message != null) + { + return message.replace(' ', '\u00A0'); + } + + return null; + } + + @Override + public void keyPressed(KeyEvent e) + { + if (e.getKeyCode() != CYCLE_HOTKEY || !config.pmTargetCycling()) + { + return; + } + + if (client.getVar(VarClientInt.INPUT_TYPE) != InputType.PRIVATE_MESSAGE.getType()) + { + return; + } + + clientThread.invoke(() -> + { + final String target = findPreviousFriend(); + if (target == null) + { + return; + } + + final String currentMessage = client.getVar(VarClientStr.INPUT_TEXT); + + client.runScript(ScriptID.OPEN_PRIVATE_MESSAGE_INTERFACE, target); + + client.setVar(VarClientStr.INPUT_TEXT, currentMessage); + client.runScript(ScriptID.CHAT_TEXT_INPUT_REBUILD, ""); + }); + } + + @Override + public void keyTyped(KeyEvent e) + { + } + + @Override + public void keyReleased(KeyEvent e) + { + } + + private String findPreviousFriend() + { + final String currentTarget = client.getVar(VarClientStr.PRIVATE_MESSAGE_TARGET); + if (currentTarget == null || friends.isEmpty()) + { + return null; + } + + for (Iterator it = friends.descendingIterator(); it.hasNext(); ) + { + String friend = it.next(); + if (friend.equals(currentTarget)) + { + return it.hasNext() ? it.next() : friends.getLast(); + } + } + + return friends.getLast(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatboxTab.java b/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatboxTab.java new file mode 100644 index 0000000000..f37adb0ea1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chathistory/ChatboxTab.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020, Anthony + * 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.chathistory; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import javax.annotation.Nullable; +import lombok.Getter; +import net.runelite.api.ChatMessageType; +import net.runelite.api.widgets.WidgetInfo; + +@Getter +enum ChatboxTab +{ + + ALL("All", "Switch tab", WidgetInfo.CHATBOX_TAB_ALL, + ChatMessageType.values()), + + // null 'after' var since we're not adding to menu + PRIVATE("Private", null, WidgetInfo.CHATBOX_TAB_PRIVATE, + ChatMessageType.PRIVATECHAT, ChatMessageType.PRIVATECHATOUT, ChatMessageType.MODPRIVATECHAT, + ChatMessageType.LOGINLOGOUTNOTIFICATION), + + // null 'after' var since we're not adding to menu + PUBLIC("Public", null, WidgetInfo.CHATBOX_TAB_PUBLIC, + ChatMessageType.PUBLICCHAT, ChatMessageType.AUTOTYPER, ChatMessageType.MODCHAT, ChatMessageType.MODAUTOTYPER), + + GAME("Game", "Game: Filter", WidgetInfo.CHATBOX_TAB_GAME, + ChatMessageType.GAMEMESSAGE, ChatMessageType.ENGINE, ChatMessageType.BROADCAST, + ChatMessageType.SNAPSHOTFEEDBACK, ChatMessageType.ITEM_EXAMINE, ChatMessageType.NPC_EXAMINE, + ChatMessageType.OBJECT_EXAMINE, ChatMessageType.FRIENDNOTIFICATION, ChatMessageType.IGNORENOTIFICATION, + ChatMessageType.CONSOLE, ChatMessageType.SPAM, ChatMessageType.PLAYERRELATED, ChatMessageType.TENSECTIMEOUT, + ChatMessageType.WELCOME, ChatMessageType.UNKNOWN), + + CLAN("Clan", "Clan: Off", WidgetInfo.CHATBOX_TAB_CLAN, + ChatMessageType.FRIENDSCHATNOTIFICATION, ChatMessageType.FRIENDSCHAT, ChatMessageType.CHALREQ_FRIENDSCHAT), + + TRADE("Trade", "Trade: Off", WidgetInfo.CHATBOX_TAB_TRADE, + ChatMessageType.TRADE_SENT, ChatMessageType.TRADEREQ, ChatMessageType.TRADE, ChatMessageType.CHALREQ_TRADE), + ; + + private static final Map TAB_MESSAGE_TYPES; + + private final String name; + @Nullable + private final String after; + private final int widgetId; + private final ChatMessageType[] messageTypes; + + ChatboxTab(String name, String after, WidgetInfo widgetId, ChatMessageType... messageTypes) + { + this.name = name; + this.after = after; + this.widgetId = widgetId.getId(); + this.messageTypes = messageTypes; + } + + static + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ChatboxTab t : values()) + { + builder.put(t.widgetId, t); + } + TAB_MESSAGE_TYPES = builder.build(); + } + + static ChatboxTab of(int widgetId) + { + return TAB_MESSAGE_TYPES.get(widgetId); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsConfig.java new file mode 100644 index 0000000000..80044fc340 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsConfig.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018, Hydrox6 + * 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.chatnotifications; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("chatnotification") +public interface ChatNotificationsConfig extends Config +{ + @ConfigItem( + position = 0, + keyName = "highlightOwnName", + name = "Highlight own name", + description = "Highlights any instance of your username in chat" + ) + default boolean highlightOwnName() + { + return true; + } + + @ConfigItem( + position = 1, + keyName = "highlightWordsString", + name = "Highlight words", + description = "Highlights the following words in chat" + ) + default String highlightWordsString() + { + return ""; + } + + @ConfigItem( + position = 2, + keyName = "notifyOnOwnName", + name = "Notify on own name", + description = "Notifies you whenever someone mentions you by name" + ) + default boolean notifyOnOwnName() + { + return false; + } + + @ConfigItem( + position = 3, + keyName = "notifyOnHighlight", + name = "Notify on highlight", + description = "Notifies you whenever a highlighted word is matched" + ) + default boolean notifyOnHighlight() + { + return false; + } + + @ConfigItem( + position = 4, + keyName = "notifyOnTrade", + name = "Notify on trade", + description = "Notifies you whenever you are traded" + ) + default boolean notifyOnTrade() + { + return false; + } + + @ConfigItem( + position = 5, + keyName = "notifyOnDuel", + name = "Notify on duel", + description = "Notifies you whenever you are challenged to a duel" + ) + default boolean notifyOnDuel() + { + return false; + } + + @ConfigItem( + position = 6, + keyName = "notifyOnBroadcast", + name = "Notify on broadcast", + description = "Notifies you whenever you receive a broadcast message" + ) + default boolean notifyOnBroadcast() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsPlugin.java new file mode 100644 index 0000000000..44069b177d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsPlugin.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2018, Hydrox6 + * 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.chatnotifications; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.inject.Provides; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.inject.Inject; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.MessageNode; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameStateChanged; +import net.runelite.client.Notifier; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.chat.ChatColorType; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.Text; + +@PluginDescriptor( + name = "Chat Notifications", + description = "Highlight and notify you of chat messages", + tags = {"duel", "messages", "notifications", "trade", "username"}, + enabledByDefault = false +) +public class ChatNotificationsPlugin extends Plugin +{ + @Inject + private Client client; + + @Inject + private ChatNotificationsConfig config; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private Notifier notifier; + + //Custom Highlights + private Pattern usernameMatcher = null; + private String usernameReplacer = ""; + private Pattern highlightMatcher = null; + + @Provides + ChatNotificationsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(ChatNotificationsConfig.class); + } + + @Override + public void startUp() + { + updateHighlights(); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case LOGIN_SCREEN: + case HOPPING: + usernameMatcher = null; + break; + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals("chatnotification")) + { + updateHighlights(); + } + } + + private void updateHighlights() + { + highlightMatcher = null; + + if (!config.highlightWordsString().trim().equals("")) + { + List items = Text.fromCSV(config.highlightWordsString()); + String joined = items.stream() + .map(Text::escapeJagex) // we compare these strings to the raw Jagex ones + .map(this::quoteAndIgnoreColor) // regex escape and ignore nested colors in the target message + .collect(Collectors.joining("|")); + // To match \b doesn't work due to <> not being in \w, + // so match \b or \s, as well as \A and \z for beginning and end of input respectively + highlightMatcher = Pattern.compile("(?:\\b|(?<=\\s)|\\A)(?:" + joined + ")(?:\\b|(?=\\s)|\\z)", Pattern.CASE_INSENSITIVE); + } + } + + @Subscribe + public void onChatMessage(ChatMessage chatMessage) + { + MessageNode messageNode = chatMessage.getMessageNode(); + boolean update = false; + + switch (chatMessage.getType()) + { + case TRADEREQ: + if (chatMessage.getMessage().contains("wishes to trade with you.") && config.notifyOnTrade()) + { + notifier.notify(chatMessage.getMessage()); + } + break; + case CHALREQ_TRADE: + if (chatMessage.getMessage().contains("wishes to duel with you.") && config.notifyOnDuel()) + { + notifier.notify(chatMessage.getMessage()); + } + break; + case BROADCAST: + if (config.notifyOnBroadcast()) + { + // Some broadcasts have links attached, notated by `|` followed by a number, while others contain color tags. + // We don't want to see either in the printed notification. + String broadcast = chatMessage.getMessage(); + + int urlTokenIndex = broadcast.lastIndexOf('|'); + if (urlTokenIndex != -1) + { + broadcast = broadcast.substring(0, urlTokenIndex); + } + + notifier.notify(Text.removeFormattingTags(broadcast)); + } + break; + case CONSOLE: + // Don't notify for notification messages + if (chatMessage.getName().equals(RuneLiteProperties.getTitle())) + { + return; + } + break; + } + + if (usernameMatcher == null && client.getLocalPlayer() != null && client.getLocalPlayer().getName() != null) + { + String username = client.getLocalPlayer().getName(); + String pattern = Arrays.stream(username.split(" ")) + .map(s -> s.isEmpty() ? "" : Pattern.quote(s)) + .collect(Collectors.joining("[\u00a0\u0020]")); // space or nbsp + usernameMatcher = Pattern.compile("\\b" + pattern + "\\b", Pattern.CASE_INSENSITIVE); + usernameReplacer = "" + username + ""; + } + + if (config.highlightOwnName() && usernameMatcher != null) + { + Matcher matcher = usernameMatcher.matcher(messageNode.getValue()); + if (matcher.find()) + { + messageNode.setValue(matcher.replaceAll(usernameReplacer)); + update = true; + if (config.notifyOnOwnName() && (chatMessage.getType() == ChatMessageType.PUBLICCHAT + || chatMessage.getType() == ChatMessageType.PRIVATECHAT + || chatMessage.getType() == ChatMessageType.FRIENDSCHAT + || chatMessage.getType() == ChatMessageType.MODCHAT + || chatMessage.getType() == ChatMessageType.MODPRIVATECHAT)) + { + sendNotification(chatMessage); + } + } + } + + if (highlightMatcher != null) + { + String nodeValue = messageNode.getValue(); + Matcher matcher = highlightMatcher.matcher(nodeValue); + boolean found = false; + StringBuffer stringBuffer = new StringBuffer(); + + while (matcher.find()) + { + String value = matcher.group(); + + // Determine the ending color by: + // 1) use the color from value if it has one + // 2) use the last color from stringBuffer + + // To do #2 we just search for the last col tag after calling appendReplacement + String endColor = getLastColor(value); + + // Strip color tags from the highlighted region so that it remains highlighted correctly + value = stripColor(value); + + matcher.appendReplacement(stringBuffer, "' + value); + + if (endColor == null) + { + endColor = getLastColor(stringBuffer.toString()); + } + + // Append end color + stringBuffer.append(endColor == null ? "" : endColor); + + update = true; + found = true; + } + + if (found) + { + matcher.appendTail(stringBuffer); + messageNode.setValue(stringBuffer.toString()); + + if (config.notifyOnHighlight()) + { + sendNotification(chatMessage); + } + } + } + + if (update) + { + messageNode.setRuneLiteFormatMessage(messageNode.getValue()); + chatMessageManager.update(messageNode); + } + } + + private void sendNotification(ChatMessage message) + { + String name = Text.removeTags(message.getName()); + String sender = message.getSender(); + StringBuilder stringBuilder = new StringBuilder(); + + if (!Strings.isNullOrEmpty(sender)) + { + stringBuilder.append('[').append(sender).append("] "); + } + + if (!Strings.isNullOrEmpty(name)) + { + stringBuilder.append(name).append(": "); + } + + stringBuilder.append(Text.removeTags(message.getMessage())); + String notification = stringBuilder.toString(); + notifier.notify(notification); + } + + private String quoteAndIgnoreColor(String str) + { + StringBuilder stringBuilder = new StringBuilder(); + + for (int i = 0; i < str.length(); ++i) + { + char c = str.charAt(i); + stringBuilder.append(Pattern.quote(String.valueOf(c))); + stringBuilder.append("(?:]*?>)?"); + } + + return stringBuilder.toString(); + } + + /** + * Get the last color tag from a string, or null if there was none + * + * @param str + * @return + */ + private static String getLastColor(String str) + { + int colIdx = str.lastIndexOf(""); + + if (colEndIdx > colIdx) + { + // ends in a which resets the color to normal + return ""; + } + + if (colIdx == -1) + { + return null; // no color + } + + int closeIdx = str.indexOf('>', colIdx); + if (closeIdx == -1) + { + return null; // unclosed col tag + } + + return str.substring(colIdx, closeIdx + 1); // include the > + } + + /** + * Strip color tags from a string. + * + * @param str + * @return + */ + @VisibleForTesting + static String stripColor(String str) + { + return str.replaceAll("(|)", ""); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelConfig.java new file mode 100644 index 0000000000..ce4e440eae --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018, Brett Middle + * 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.combatlevel; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("combatlevel") +public interface CombatLevelConfig extends Config +{ + @ConfigItem( + keyName = "showLevelsUntil", + name = "Calculate next level", + description = "Mouse over the combat level to calculate what skill levels will increase combat." + ) + default boolean showLevelsUntil() + { + return true; + } + + @ConfigItem( + keyName = "showPreciseCombatLevel", + name = "Show precise combat level", + description = "Displays your combat level with accurate decimals." + ) + default boolean showPreciseCombatLevel() + { + return true; + } + + @ConfigItem( + keyName = "wildernessAttackLevelRange", + name = "Show level range in wilderness", + description = "Displays a PVP-world-like attack level range in the wilderness" + ) + default boolean wildernessAttackLevelRange() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelOverlay.java new file mode 100644 index 0000000000..0eabb100a1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelOverlay.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2018, Brett Middle + * 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.combatlevel; + +import com.google.common.annotations.VisibleForTesting; +import net.runelite.api.Client; +import net.runelite.api.Experience; +import net.runelite.api.Skill; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.tooltip.Tooltip; +import net.runelite.client.ui.overlay.tooltip.TooltipManager; +import net.runelite.client.util.ColorUtil; +import javax.inject.Inject; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +class CombatLevelOverlay extends Overlay +{ + private static final Color COMBAT_LEVEL_COLOUR = new Color(0xff981f); + + private final Client client; + private final CombatLevelConfig config; + private final TooltipManager tooltipManager; + + @Inject + private CombatLevelOverlay(Client client, CombatLevelConfig config, TooltipManager tooltipManager) + { + this.client = client; + this.config = config; + this.tooltipManager = tooltipManager; + } + + @Override + public Dimension render(Graphics2D graphics) + { + Widget combatLevelWidget = client.getWidget(WidgetInfo.COMBAT_LEVEL); + if (!config.showLevelsUntil() + || client.getLocalPlayer().getCombatLevel() == Experience.MAX_COMBAT_LEVEL + || combatLevelWidget == null || combatLevelWidget.isHidden()) + { + return null; + } + + Rectangle combatCanvas = combatLevelWidget.getBounds(); + + if (combatCanvas == null) + { + return null; + } + + if (combatCanvas.contains(client.getMouseCanvasPosition().getX(), client.getMouseCanvasPosition().getY())) + { + tooltipManager.add(new Tooltip(getLevelsUntilTooltip())); + } + + return null; + } + + @VisibleForTesting + String getLevelsUntilTooltip() + { + // grab combat skills from player + int attackLevel = client.getRealSkillLevel(Skill.ATTACK); + int strengthLevel = client.getRealSkillLevel(Skill.STRENGTH); + int defenceLevel = client.getRealSkillLevel(Skill.DEFENCE); + int hitpointsLevel = client.getRealSkillLevel(Skill.HITPOINTS); + int magicLevel = client.getRealSkillLevel(Skill.MAGIC); + int rangeLevel = client.getRealSkillLevel(Skill.RANGED); + int prayerLevel = client.getRealSkillLevel(Skill.PRAYER); + + // find the needed levels until level up + int meleeNeed = Experience.getNextCombatLevelMelee(attackLevel, strengthLevel, defenceLevel, hitpointsLevel, + magicLevel, rangeLevel, prayerLevel); + int hpDefNeed = Experience.getNextCombatLevelHpDef(attackLevel, strengthLevel, defenceLevel, hitpointsLevel, + magicLevel, rangeLevel, prayerLevel); + int rangeNeed = Experience.getNextCombatLevelRange(attackLevel, strengthLevel, defenceLevel, hitpointsLevel, + magicLevel, rangeLevel, prayerLevel); + int magicNeed = Experience.getNextCombatLevelMagic(attackLevel, strengthLevel, defenceLevel, hitpointsLevel, + magicLevel, rangeLevel, prayerLevel); + int prayerNeed = Experience.getNextCombatLevelPrayer(attackLevel, strengthLevel, defenceLevel, hitpointsLevel, + magicLevel, rangeLevel, prayerLevel); + + // create tooltip string + StringBuilder sb = new StringBuilder(); + sb.append(ColorUtil.wrapWithColorTag("Next combat level:
", COMBAT_LEVEL_COLOUR)); + + if ((attackLevel + strengthLevel) < Experience.MAX_REAL_LEVEL * 2) + { + sb.append(meleeNeed).append(" Attack/Strength
"); + } + if ((hitpointsLevel + defenceLevel) < Experience.MAX_REAL_LEVEL * 2) + { + sb.append(hpDefNeed).append(" Defence/Hitpoints
"); + } + if (rangeLevel < Experience.MAX_REAL_LEVEL) + { + sb.append(rangeNeed).append(" Ranged
"); + } + if (magicLevel < Experience.MAX_REAL_LEVEL) + { + sb.append(magicNeed).append(" Magic
"); + } + if (prayerLevel < Experience.MAX_REAL_LEVEL) + { + sb.append(prayerNeed).append(" Prayer"); + } + return sb.toString(); + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelPlugin.java new file mode 100644 index 0000000000..0d401d8e13 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/combatlevel/CombatLevelPlugin.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2017, Devin French + * Copyright (c) 2019, 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.combatlevel; + +import com.google.inject.Provides; +import java.text.DecimalFormat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Experience; +import net.runelite.api.GameState; +import net.runelite.api.ScriptID; +import net.runelite.api.Skill; +import net.runelite.api.WorldType; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.ScriptPostFired; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +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.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; + +@PluginDescriptor( + name = "Combat Level", + description = "Show a more accurate combat level in Combat Options panel and other combat level functions", + tags = {"wilderness", "attack", "range"} +) +public class CombatLevelPlugin extends Plugin +{ + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.###"); + private static final String CONFIG_GROUP = "combatlevel"; + private static final String ATTACK_RANGE_CONFIG_KEY = "wildernessAttackLevelRange"; + private static final Pattern WILDERNESS_LEVEL_PATTERN = Pattern.compile("^Level: (\\d+)$"); + private static final int SKULL_CONTAINER_ADJUSTED_ORIGINAL_Y = 6; + private static final int WILDERNESS_LEVEL_TEXT_ADJUSTED_ORIGINAL_Y = 3; + private static final int MIN_COMBAT_LEVEL = 3; + + private int originalWildernessLevelTextPosition = -1; + private int originalSkullContainerPosition = -1; + + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Inject + private CombatLevelConfig config; + + @Inject + private CombatLevelOverlay overlay; + + @Inject + private OverlayManager overlayManager; + + @Provides + CombatLevelConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(CombatLevelConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(overlay); + + if (config.wildernessAttackLevelRange()) + { + appendAttackLevelRangeText(); + } + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(overlay); + Widget combatLevelWidget = client.getWidget(WidgetInfo.COMBAT_LEVEL); + + if (combatLevelWidget != null) + { + String widgetText = combatLevelWidget.getText(); + + if (widgetText.contains(".")) + { + combatLevelWidget.setText(widgetText.substring(0, widgetText.indexOf("."))); + } + } + + shutDownAttackLevelRange(); + } + + @Subscribe + public void onGameTick(GameTick event) + { + if (client.getGameState() != GameState.LOGGED_IN) + { + return; + } + + Widget combatLevelWidget = client.getWidget(WidgetInfo.COMBAT_LEVEL); + if (combatLevelWidget == null || !config.showPreciseCombatLevel()) + { + return; + } + + double combatLevelPrecise = Experience.getCombatLevelPrecise( + client.getRealSkillLevel(Skill.ATTACK), + client.getRealSkillLevel(Skill.STRENGTH), + client.getRealSkillLevel(Skill.DEFENCE), + client.getRealSkillLevel(Skill.HITPOINTS), + client.getRealSkillLevel(Skill.MAGIC), + client.getRealSkillLevel(Skill.RANGED), + client.getRealSkillLevel(Skill.PRAYER) + ); + + combatLevelWidget.setText("Combat Lvl: " + DECIMAL_FORMAT.format(combatLevelPrecise)); + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!CONFIG_GROUP.equals(event.getGroup()) || !ATTACK_RANGE_CONFIG_KEY.equals(event.getKey())) + { + return; + } + + if (config.wildernessAttackLevelRange()) + { + appendAttackLevelRangeText(); + } + else + { + shutDownAttackLevelRange(); + } + } + + @Subscribe + public void onScriptPostFired(ScriptPostFired scriptPostFired) + { + if (scriptPostFired.getScriptId() == ScriptID.PVP_WIDGET_BUILDER && config.wildernessAttackLevelRange()) + { + appendAttackLevelRangeText(); + } + } + + private void appendAttackLevelRangeText() + { + final Widget wildernessLevelWidget = client.getWidget(WidgetInfo.PVP_WILDERNESS_LEVEL); + if (wildernessLevelWidget == null) + { + return; + } + + final String wildernessLevelText = wildernessLevelWidget.getText(); + final Matcher m = WILDERNESS_LEVEL_PATTERN.matcher(wildernessLevelText); + if (!m.matches() + || WorldType.isPvpWorld(client.getWorldType())) + { + return; + } + + final Widget skullContainer = client.getWidget(WidgetInfo.PVP_SKULL_CONTAINER); + if (originalWildernessLevelTextPosition == -1) + { + originalWildernessLevelTextPosition = wildernessLevelWidget.getOriginalY(); + } + if (originalSkullContainerPosition == -1) + { + originalSkullContainerPosition = skullContainer.getRelativeY(); + } + + final int wildernessLevel = Integer.parseInt(m.group(1)); + final int combatLevel = client.getLocalPlayer().getCombatLevel(); + + wildernessLevelWidget.setText(wildernessLevelText + "
" + combatAttackRange(combatLevel, wildernessLevel)); + wildernessLevelWidget.setOriginalY(WILDERNESS_LEVEL_TEXT_ADJUSTED_ORIGINAL_Y); + skullContainer.setOriginalY(SKULL_CONTAINER_ADJUSTED_ORIGINAL_Y); + + clientThread.invoke(wildernessLevelWidget::revalidate); + clientThread.invoke(skullContainer::revalidate); + } + + private void shutDownAttackLevelRange() + { + if (WorldType.isPvpWorld(client.getWorldType())) + { + return; + } + + final Widget wildernessLevelWidget = client.getWidget(WidgetInfo.PVP_WILDERNESS_LEVEL); + if (wildernessLevelWidget != null) + { + String wildernessLevelText = wildernessLevelWidget.getText(); + if (wildernessLevelText.contains("
")) + { + wildernessLevelWidget.setText(wildernessLevelText.substring(0, wildernessLevelText.indexOf("
"))); + } + wildernessLevelWidget.setOriginalY(originalWildernessLevelTextPosition); + clientThread.invoke(wildernessLevelWidget::revalidate); + } + originalWildernessLevelTextPosition = -1; + + final Widget skullContainer = client.getWidget(WidgetInfo.PVP_SKULL_CONTAINER); + if (skullContainer != null) + { + skullContainer.setOriginalY(originalSkullContainerPosition); + clientThread.invoke(skullContainer::revalidate); + } + originalSkullContainerPosition = -1; + } + + private static String combatAttackRange(final int combatLevel, final int wildernessLevel) + { + return Math.max(MIN_COMBAT_LEVEL, combatLevel - wildernessLevel) + "-" + Math.min(Experience.MAX_COMBAT_LEVEL, combatLevel + wildernessLevel); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightConfig.java new file mode 100644 index 0000000000..dd1ab5fb20 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, Morgan Lewis + * 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.mousehighlight; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("mousehighlight") +public interface MouseHighlightConfig extends Config +{ + @ConfigItem( + position = 0, + keyName = "uiTooltip", + name = "Interface Tooltips", + description = "Whether or not tooltips are shown on interfaces" + ) + default boolean uiTooltip() + { + return true; + } + + @ConfigItem( + position = 1, + keyName = "chatboxTooltip", + name = "Chatbox Tooltips", + description = "Whether or not tooltips are shown over the chatbox" + ) + default boolean chatboxTooltip() + { + return true; + } + + @ConfigItem( + position = 2, + keyName = "disableSpellbooktooltip", + name = "Disable Spellbook Tooltips", + description = "Disable Spellbook Tooltips so they don't cover descriptions" + ) + default boolean disableSpellbooktooltip() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightOverlay.java new file mode 100644 index 0000000000..07b11ccf50 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightOverlay.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2017, Aria + * 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.mousehighlight; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.util.Set; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.VarClientInt; +import net.runelite.api.widgets.WidgetID; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.tooltip.Tooltip; +import net.runelite.client.ui.overlay.tooltip.TooltipManager; + +class MouseHighlightOverlay extends Overlay +{ + /** + * Menu types which are on widgets. + */ + private static final Set WIDGET_MENU_ACTIONS = ImmutableSet.of( + MenuAction.WIDGET_TYPE_1, + MenuAction.WIDGET_TYPE_2, + MenuAction.WIDGET_TYPE_3, + MenuAction.WIDGET_TYPE_4, + MenuAction.WIDGET_TYPE_5, + MenuAction.WIDGET_TYPE_6, + MenuAction.ITEM_USE_ON_WIDGET_ITEM, + MenuAction.ITEM_USE_ON_WIDGET, + MenuAction.ITEM_FIRST_OPTION, + MenuAction.ITEM_SECOND_OPTION, + MenuAction.ITEM_THIRD_OPTION, + MenuAction.ITEM_FOURTH_OPTION, + MenuAction.ITEM_FIFTH_OPTION, + MenuAction.ITEM_USE, + MenuAction.ITEM_DROP, + MenuAction.WIDGET_FIRST_OPTION, + MenuAction.WIDGET_SECOND_OPTION, + MenuAction.WIDGET_THIRD_OPTION, + MenuAction.WIDGET_FOURTH_OPTION, + MenuAction.WIDGET_FIFTH_OPTION, + MenuAction.EXAMINE_ITEM, + MenuAction.SPELL_CAST_ON_WIDGET, + MenuAction.CC_OP_LOW_PRIORITY, + MenuAction.CC_OP + ); + + private final TooltipManager tooltipManager; + private final Client client; + private final MouseHighlightConfig config; + + @Inject + MouseHighlightOverlay(Client client, TooltipManager tooltipManager, MouseHighlightConfig config) + { + setPosition(OverlayPosition.DYNAMIC); + this.client = client; + this.tooltipManager = tooltipManager; + this.config = config; + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (client.isMenuOpen()) + { + return null; + } + + MenuEntry[] menuEntries = client.getMenuEntries(); + int last = menuEntries.length - 1; + + if (last < 0) + { + return null; + } + + MenuEntry menuEntry = menuEntries[last]; + String target = menuEntry.getTarget(); + String option = menuEntry.getOption(); + MenuAction type = MenuAction.of(menuEntry.getType()); + + if (type == MenuAction.RUNELITE_OVERLAY || type == MenuAction.CC_OP_LOW_PRIORITY) + { + // These are always right click only + return null; + } + + if (Strings.isNullOrEmpty(option)) + { + return null; + } + + // Trivial options that don't need to be highlighted, add more as they appear. + switch (option) + { + case "Walk here": + case "Cancel": + case "Continue": + return null; + case "Move": + // Hide overlay on sliding puzzle boxes + if (target.contains("Sliding piece")) + { + return null; + } + } + + if (WIDGET_MENU_ACTIONS.contains(type)) + { + final int widgetId = menuEntry.getParam1(); + final int groupId = WidgetInfo.TO_GROUP(widgetId); + + if (!config.uiTooltip()) + { + return null; + } + + if (!config.chatboxTooltip() && groupId == WidgetInfo.CHATBOX.getGroupId()) + { + return null; + } + + if (config.disableSpellbooktooltip() && groupId == WidgetID.SPELLBOOK_GROUP_ID) + { + return null; + } + } + + // If this varc is set, a tooltip will be displayed soon + int tooltipTimeout = client.getVar(VarClientInt.TOOLTIP_TIMEOUT); + if (tooltipTimeout > client.getGameCycle()) + { + return null; + } + + // If this varc is set, a tooltip is already being displayed + int tooltipDisplayed = client.getVar(VarClientInt.TOOLTIP_VISIBLE); + if (tooltipDisplayed == 1) + { + return null; + } + + tooltipManager.addFront(new Tooltip(option + (Strings.isNullOrEmpty(target) ? "" : " " + target))); + return null; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightPlugin.java new file mode 100644 index 0000000000..8881b92d93 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/mousehighlight/MouseHighlightPlugin.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017, Aria + * 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.mousehighlight; + +import com.google.inject.Provides; +import javax.inject.Inject; +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; + +@PluginDescriptor( + name = "Mouse Tooltips", + description = "Render default actions as a tooltip", + tags = {"actions", "overlay"} +) +public class MouseHighlightPlugin extends Plugin +{ + @Inject + private OverlayManager overlayManager; + + @Inject + private MouseHighlightOverlay overlay; + + @Provides + MouseHighlightConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(MouseHighlightConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(overlay); + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(overlay); + } +}