From 5d7bf6aa54940ea285db1f88ba352a2fb4133cb6 Mon Sep 17 00:00:00 2001 From: therealunull Date: Wed, 16 Dec 2020 15:28:24 -0500 Subject: [PATCH] mes, metronome, npchighlight, npcunaggroarea, objectindicators --- .../main/java/net/runelite/api/Client.java | 2 +- .../java/net/runelite/api/Preferences.java | 8 +- .../net/runelite/api/coords/WorldPoint.java | 28 +- .../net/runelite/api/widgets/WidgetInfo.java | 2 +- .../chathistory/ChatHistoryPlugin.java | 4 +- .../plugins/devtools/ScriptInspector.java | 4 +- .../plugins/devtools/WidgetInspector.java | 8 +- .../client/plugins/examine/ExaminePlugin.java | 6 +- .../friendnotes/FriendNotesPlugin.java | 4 +- .../grandexchange/GrandExchangePlugin.java | 2 +- .../client/plugins/hiscore/HiscorePlugin.java | 2 +- .../plugins/itemprices/ItemPricesOverlay.java | 2 +- .../plugins/itemstats/ItemStatOverlay.java | 2 +- .../plugins/menuentryswapper/BuyMode.java | 34 + .../menuentryswapper/FairyRingMode.java | 46 + .../menuentryswapper/GEItemCollectMode.java | 46 + .../HouseAdvertisementMode.java | 45 + .../plugins/menuentryswapper/HouseMode.java | 47 + .../MenuEntrySwapperConfig.java | 609 +++++++++++++ .../MenuEntrySwapperPlugin.java | 851 ++++++++++++++++++ .../plugins/menuentryswapper/SellMode.java | 34 + .../menuentryswapper/ShiftDepositMode.java | 52 ++ .../menuentryswapper/ShiftWithdrawMode.java | 55 ++ .../client/plugins/menuentryswapper/Swap.java | 39 + .../plugins/metronome/MetronomePlugin.java | 100 ++ .../MetronomePluginConfiguration.java | 74 ++ .../mousehighlight/MouseHighlightOverlay.java | 2 +- .../plugins/npchighlight/MemorizedNpc.java | 79 ++ .../npchighlight/NpcIndicatorsConfig.java | 171 ++++ .../npchighlight/NpcIndicatorsPlugin.java | 662 ++++++++++++++ .../npchighlight/NpcMinimapOverlay.java | 85 ++ .../plugins/npchighlight/NpcSceneOverlay.java | 206 +++++ .../npcunaggroarea/AggressionTimer.java | 68 ++ .../npcunaggroarea/NpcAggroAreaConfig.java | 120 +++ .../NpcAggroAreaNotWorkingOverlay.java | 63 ++ .../npcunaggroarea/NpcAggroAreaOverlay.java | 115 +++ .../npcunaggroarea/NpcAggroAreaPlugin.java | 492 ++++++++++ .../objectindicators/ColorTileObject.java | 42 + .../ObjectIndicatorsConfig.java | 57 ++ .../ObjectIndicatorsOverlay.java | 119 +++ .../ObjectIndicatorsPlugin.java | 462 ++++++++++ .../plugins/objectindicators/ObjectPoint.java | 45 + .../puzzlesolver/PuzzleSolverPlugin.java | 4 +- .../ScreenMarkerWidgetHighlightOverlay.java | 2 +- .../client/plugins/timers/TimersPlugin.java | 2 +- .../plugins/xptracker/XpTrackerPlugin.java | 6 +- .../client/ui/overlay/WidgetItemOverlay.java | 4 +- .../net/runelite/mixins/RSClientMixin.java | 2 +- .../net/runelite/mixins/RSWidgetMixin.java | 18 +- .../net/runelite/mixins/SoundEffectMixin.java | 2 +- .../java/net/runelite/rs/api/RSClient.java | 2 +- .../runelite/rs/api/RSClientPreferences.java | 8 +- 52 files changed, 4885 insertions(+), 59 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/BuyMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/FairyRingMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/GEItemCollectMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseAdvertisementMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/SellMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftDepositMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftWithdrawMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/Swap.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePluginConfiguration.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcMinimapOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ColorTileObject.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectPoint.java diff --git a/runelite-api/src/main/java/net/runelite/api/Client.java b/runelite-api/src/main/java/net/runelite/api/Client.java index e5d177b37a..bdb614c17b 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -1807,7 +1807,7 @@ public interface Client extends GameShell /** * Returns client item composition cache */ - NodeCache getItemDefinitionCache(); + NodeCache getItemCompositionCache(); /** * Returns the array of cross sprites that appear and animate when left-clicking diff --git a/runelite-api/src/main/java/net/runelite/api/Preferences.java b/runelite-api/src/main/java/net/runelite/api/Preferences.java index 46cff0b1e3..b134b96bdf 100644 --- a/runelite-api/src/main/java/net/runelite/api/Preferences.java +++ b/runelite-api/src/main/java/net/runelite/api/Preferences.java @@ -43,13 +43,13 @@ public interface Preferences */ void setRememberedUsername(String username); - int getSoundEffectsVolume(); + int getSoundEffectVolume(); - void setSoundEffectsVolume(int i); + void setSoundEffectVolume(int i); - int getAreaSoundEffectsVolume(); + int getAreaSoundEffectVolume(); - void setAreaSoundEffectsVolume(int i); + void setAreaSoundEffectVolume(int i); int getMusicVolume(); diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java index c41b6723da..7cad194077 100644 --- a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java +++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java @@ -159,12 +159,25 @@ public class WorldPoint * Gets the coordinate of the tile that contains the passed local point, * accounting for instances. * - * @param client the client + * @param client the client * @param localPoint the local coordinate * @return the tile coordinate containing the local point */ - @Nullable public static WorldPoint fromLocalInstance(Client client, LocalPoint localPoint) + { + return fromLocalInstance(client, localPoint, client.getPlane()); + } + + /** + * Gets the coordinate of the tile that contains the passed local point, + * accounting for instances. + * + * @param client the client + * @param localPoint the local coordinate + * @param plane the plane for the returned point, if it is not an instance + * @return the tile coordinate containing the local point + */ + public static WorldPoint fromLocalInstance(Client client, LocalPoint localPoint, int plane) { if (client.isInInstancedRegion()) { @@ -176,11 +189,6 @@ public class WorldPoint int chunkX = sceneX / CHUNK_SIZE; int chunkY = sceneY / CHUNK_SIZE; - if (chunkX >= 13 || chunkY >= 13) - { - return null; - } - // get the template chunk for the chunk int[][][] instanceTemplateChunks = client.getInstanceTemplateChunks(); int templateChunk = instanceTemplateChunks[client.getPlane()][chunkX][chunkY]; @@ -188,18 +196,18 @@ public class WorldPoint int rotation = templateChunk >> 1 & 0x3; int templateChunkY = (templateChunk >> 3 & 0x7FF) * CHUNK_SIZE; int templateChunkX = (templateChunk >> 14 & 0x3FF) * CHUNK_SIZE; - int plane = templateChunk >> 24 & 0x3; + int templateChunkPlane = templateChunk >> 24 & 0x3; // calculate world point of the template int x = templateChunkX + (sceneX & (CHUNK_SIZE - 1)); int y = templateChunkY + (sceneY & (CHUNK_SIZE - 1)); // create and rotate point back to 0, to match with template - return rotate(new WorldPoint(x, y, plane), 4 - rotation); + return rotate(new WorldPoint(x, y, templateChunkPlane), 4 - rotation); } else { - return fromLocal(client, localPoint); + return fromLocal(client, localPoint.getX(), localPoint.getY(), plane); } } diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java index b4181ac199..b69c6d45b7 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java @@ -981,7 +981,7 @@ public enum WidgetInfo * @param id passed group-child ID * @return the group ID */ - public static int getGroupFromID(int id) + public static int TO_GROUP(int id) { return id >>> 16; } 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 index 9cc102e2b3..6526cce6eb 100644 --- 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 @@ -54,7 +54,7 @@ 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.getChildFromID; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +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; @@ -203,7 +203,7 @@ public class ChatHistoryPlugin extends Plugin implements KeyListener return; } - final int groupId = getGroupFromID(entry.getParam1()); + final int groupId = TO_GROUP(entry.getParam1()); final int childId = getChildFromID(entry.getParam1()); if (groupId != WidgetInfo.CHATBOX.getGroupId()) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ScriptInspector.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ScriptInspector.java index 678dd7a4f3..5dacdc556f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ScriptInspector.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ScriptInspector.java @@ -64,7 +64,7 @@ import net.runelite.api.events.ScriptPreFired; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import static net.runelite.api.widgets.WidgetInfo.getChildFromID; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; @@ -120,7 +120,7 @@ public class ScriptInspector extends JFrame if (source != null) { int id = source.getId(); - output += " - " + getGroupFromID(id) + "." + getChildFromID(id); + output += " - " + TO_GROUP(id) + "." + getChildFromID(id); if (source.getIndex() != -1) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/WidgetInspector.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/WidgetInspector.java index 61953cc628..4e7cb56d66 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/WidgetInspector.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/WidgetInspector.java @@ -60,7 +60,7 @@ import net.runelite.api.MenuAction; import net.runelite.api.MenuEntry; import net.runelite.api.SpriteID; import static net.runelite.api.widgets.WidgetInfo.getChildFromID; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; import net.runelite.client.events.ConfigChanged; import net.runelite.api.events.MenuEntryAdded; import net.runelite.api.events.MenuOptionClicked; @@ -464,7 +464,7 @@ class WidgetInspector extends JFrame picker = parent.createChild(-1, WidgetType.GRAPHIC); - log.info("Picker is {}.{} [{}]", WidgetInfo.getGroupFromID(picker.getId()), WidgetInfo.getChildFromID(picker.getId()), picker.getIndex()); + log.info("Picker is {}.{} [{}]", WidgetInfo.TO_GROUP(picker.getId()), WidgetInfo.getChildFromID(picker.getId()), picker.getIndex()); picker.setSpriteId(SpriteID.MOBILE_FINGER_ON_INTERFACE); picker.setOriginalWidth(15); @@ -538,7 +538,7 @@ class WidgetInspector extends JFrame { continue; } - String name = WidgetInfo.getGroupFromID(entry.getParam1()) + "." + WidgetInfo.getChildFromID(entry.getParam1()); + String name = WidgetInfo.TO_GROUP(entry.getParam1()) + "." + WidgetInfo.getChildFromID(entry.getParam1()); if (entry.getParam0() != -1) { @@ -584,7 +584,7 @@ class WidgetInspector extends JFrame public static String getWidgetIdentifier(Widget widget) { int id = widget.getId(); - String str = getGroupFromID(id) + "." + getChildFromID(id); + String str = TO_GROUP(id) + "." + getChildFromID(id); if (widget.getIndex() != -1) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java index b31a7b2731..6db8c8512b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java @@ -46,7 +46,7 @@ import net.runelite.api.widgets.WidgetID; import net.runelite.api.widgets.WidgetInfo; import static net.runelite.api.widgets.WidgetInfo.SEED_VAULT_ITEM_CONTAINER; import static net.runelite.api.widgets.WidgetInfo.getChildFromID; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; import net.runelite.api.widgets.WidgetItem; import net.runelite.client.chat.ChatColorType; import net.runelite.client.chat.ChatMessageBuilder; @@ -123,7 +123,7 @@ public class ExaminePlugin extends Plugin id = event.getId(); int widgetId = event.getWidgetId(); - int widgetGroup = getGroupFromID(widgetId); + int widgetGroup = TO_GROUP(widgetId); int widgetChild = getChildFromID(widgetId); Widget widget = client.getWidget(widgetGroup, widgetChild); WidgetItem widgetItem = widget.getWidgetItem(event.getActionParam()); @@ -266,7 +266,7 @@ public class ExaminePlugin extends Plugin private int[] findItemFromWidget(int widgetId, int actionParam) { - int widgetGroup = getGroupFromID(widgetId); + int widgetGroup = TO_GROUP(widgetId); int widgetChild = getChildFromID(widgetId); Widget widget = client.getWidget(widgetGroup, widgetChild); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java index de0fe4b9dc..c3b4426a2a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java @@ -235,7 +235,7 @@ public class FriendNotesPlugin extends Plugin @Subscribe public void onMenuEntryAdded(MenuEntryAdded event) { - final int groupId = WidgetInfo.getGroupFromID(event.getActionParam1()); + final int groupId = WidgetInfo.TO_GROUP(event.getActionParam1()); // Look for "Message" on friends list if ((groupId == WidgetInfo.FRIENDS_LIST.getGroupId() && event.getOption().equals("Message")) || @@ -265,7 +265,7 @@ public class FriendNotesPlugin extends Plugin @Subscribe public void onMenuOptionClicked(MenuOptionClicked event) { - final int groupId = WidgetInfo.getGroupFromID(event.getWidgetId()); + final int groupId = WidgetInfo.TO_GROUP(event.getWidgetId()); if (groupId == WidgetInfo.FRIENDS_LIST.getGroupId() || groupId == WidgetInfo.IGNORE_LIST.getGroupId()) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java index 8b62273426..b6ee03e21d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java @@ -557,7 +557,7 @@ public class GrandExchangePlugin extends Plugin final MenuEntry[] entries = client.getMenuEntries(); final MenuEntry menuEntry = entries[entries.length - 1]; final int widgetId = menuEntry.getParam1(); - final int groupId = WidgetInfo.getGroupFromID(widgetId); + final int groupId = WidgetInfo.TO_GROUP(widgetId); switch (groupId) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java index 7dcf181c99..75e41f6018 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java @@ -148,7 +148,7 @@ public class HiscorePlugin extends Plugin return; } - int groupId = WidgetInfo.getGroupFromID(event.getActionParam1()); + int groupId = WidgetInfo.TO_GROUP(event.getActionParam1()); String option = event.getOption(); if (groupId == WidgetInfo.FRIENDS_LIST.getGroupId() || groupId == WidgetInfo.FRIENDS_CHAT.getGroupId() || diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesOverlay.java index dac17ee36e..c614665401 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesOverlay.java @@ -91,7 +91,7 @@ class ItemPricesOverlay extends Overlay final MenuEntry menuEntry = menuEntries[last]; final MenuAction action = MenuAction.of(menuEntry.getType()); final int widgetId = menuEntry.getParam1(); - final int groupId = WidgetInfo.getGroupFromID(widgetId); + final int groupId = WidgetInfo.TO_GROUP(widgetId); final boolean isAlching = menuEntry.getOption().equals("Cast") && menuEntry.getTarget().contains("High Level Alchemy"); // Tooltip action type handling diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java index bfcbb8e58d..e6315c89ce 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java @@ -92,7 +92,7 @@ public class ItemStatOverlay extends Overlay } final MenuEntry entry = menu[menuSize - 1]; - final int group = WidgetInfo.getGroupFromID(entry.getParam1()); + final int group = WidgetInfo.TO_GROUP(entry.getParam1()); final int child = WidgetInfo.getChildFromID(entry.getParam1()); final Widget widget = client.getWidget(group, child); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/BuyMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/BuyMode.java new file mode 100644 index 0000000000..b3f79c43d1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/BuyMode.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020, Sam + * 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.menuentryswapper; + +public enum BuyMode +{ + OFF, + BUY_1, + BUY_5, + BUY_10, + BUY_50; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/FairyRingMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/FairyRingMode.java new file mode 100644 index 0000000000..731561a9a6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/FairyRingMode.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018, Ethan + * 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.menuentryswapper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FairyRingMode +{ + ZANARIS("Zanaris"), + LAST_DESTINATION("Last-Destination"), + CONFIGURE("Configure"), + OFF("Off"); + + private final String name; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/GEItemCollectMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/GEItemCollectMode.java new file mode 100644 index 0000000000..e15277e320 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/GEItemCollectMode.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019, Rami + * 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.menuentryswapper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum GEItemCollectMode +{ + DEFAULT("Default"), + ITEMS("Collect-items"), + NOTES("Collect-notes"), + BANK("Bank"); + + private final String name; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseAdvertisementMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseAdvertisementMode.java new file mode 100644 index 0000000000..98860253bd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseAdvertisementMode.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019, 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.menuentryswapper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum HouseAdvertisementMode +{ + VIEW("View"), + ADD_HOUSE("Add-House"), + VISIT_LAST("Visit-Last"); + + private final String name; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseMode.java new file mode 100644 index 0000000000..c445408f9e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/HouseMode.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018, Snakk + * 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.menuentryswapper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum HouseMode +{ + ENTER("Enter"), + HOME("Home"), + BUILD_MODE("Build mode"), + FRIENDS_HOUSE("Friend's House"); + + private final String name; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java new file mode 100644 index 0000000000..33ac662638 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java @@ -0,0 +1,609 @@ +/* + * 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.menuentryswapper; + +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(MenuEntrySwapperConfig.GROUP) +public interface MenuEntrySwapperConfig extends Config +{ + String GROUP = "menuentryswapper"; + + @ConfigSection( + name = "Item Swaps", + description = "All options that swap item menu entries", + position = 0, + closedByDefault = true + ) + String itemSection = "items"; + + @ConfigSection( + name = "NPC Swaps", + description = "All options that swap NPC menu entries", + position = 1, + closedByDefault = true + ) + String npcSection = "npcs"; + + @ConfigSection( + name = "Object Swaps", + description = "All options that swap object menu entries", + position = 2, + closedByDefault = true + ) + String objectSection = "objects"; + + @ConfigSection( + name = "UI Swaps", + description = "All options that swap entries in the UI (except Items)", + position = 3, + closedByDefault = true + ) + String uiSection = "ui"; + + @ConfigItem( + position = -2, + keyName = "shiftClickCustomization", + name = "Customizable shift-click", + description = "Allows customization of shift-clicks on items", + section = itemSection + ) + default boolean shiftClickCustomization() + { + return true; + } + + @ConfigItem( + keyName = "swapAdmire", + name = "Admire", + description = "Swap Admire with Teleport, Spellbook and Perks (max cape) for mounted skill capes.", + section = objectSection + ) + default boolean swapAdmire() + { + return true; + } + + @ConfigItem( + keyName = "swapAssignment", + name = "Assignment", + description = "Swap Talk-to with Assignment for Slayer Masters. This will take priority over swapping Trade.", + section = npcSection + ) + default boolean swapAssignment() + { + return true; + } + + @ConfigItem( + keyName = "swapBanker", + name = "Bank", + description = "Swap Talk-to with Bank on Bank NPC
Example: Banker", + section = npcSection + ) + default boolean swapBank() + { + return true; + } + + @ConfigItem( + keyName = "swapBirdhouseEmpty", + name = "Birdhouse", + description = "Swap Interact with Empty for birdhouses on Fossil Island", + section = objectSection + ) + default boolean swapBirdhouseEmpty() + { + return true; + } + + @ConfigItem( + keyName = "swapBones", + name = "Bury", + description = "Swap Bury with Use on Bones", + section = itemSection + ) + default boolean swapBones() + { + return false; + } + + @ConfigItem( + keyName = "swapHerbs", + name = "Clean", + description = "Swap Clean with Use on Herbs", + section = itemSection + ) + default boolean swapHerbs() + { + return false; + } + + @ConfigItem( + keyName = "swapPrayerBook", + name = "Recite-Prayer", + description = "Swap Read with Recite-prayer on the Prayer Book from The Great Brain Robbery quest", + section = itemSection + ) + default boolean swapPrayerBook() + { + return false; + } + + @ConfigItem( + keyName = "swapContract", + name = "Contract", + description = "Swap Talk-to with Contract on Guildmaster Jane", + section = npcSection + ) + default boolean swapContract() + { + return true; + } + + @ConfigItem( + keyName = "swapChase", + name = "Chase", + description = "Allows to left click your cat to chase", + section = npcSection + ) + default boolean swapChase() + { + return true; + } + + @ConfigItem( + keyName = "claimSlime", + name = "Claim Slime", + description = "Swap Talk-to with Claim Slime from Morytania diaries", + section = npcSection + ) + default boolean claimSlime() + { + return true; + } + + @ConfigItem( + keyName = "swapDarkMage", + name = "Repairs", + description = "Swap Talk-to with Repairs for Dark Mage", + section = npcSection + ) + default boolean swapDarkMage() + { + return true; + } + + @ConfigItem( + keyName = "swapCaptainKhaled", + name = "Task", + description = "Swap Talk-to with Task for Captain Khaled in Port Piscarilius", + section = npcSection + ) + default boolean swapCaptainKhaled() + { + return false; + } + + @ConfigItem( + keyName = "swapDecant", + name = "Decant", + description = "Swap Talk-to with Decant for Bob Barter and Murky Matt at the Grand Exchange.", + section = npcSection + ) + default boolean swapDecant() + { + return false; + } + + @ConfigItem( + keyName = "swapExchange", + name = "Exchange", + description = "Swap Talk-to with Exchange on NPC
Example: Grand Exchange Clerk, Tool Leprechaun, Void Knight", + section = npcSection + ) + default boolean swapExchange() + { + return true; + } + + @ConfigItem( + keyName = "swapFairyRing", + name = "Fairy ring", + description = "Swap Zanaris with Last-destination or Configure on Fairy rings", + section = objectSection + ) + default FairyRingMode swapFairyRing() + { + return FairyRingMode.LAST_DESTINATION; + } + + @ConfigItem( + keyName = "swapHardWoodGrove", + name = "Hardwood Grove Quick-Pay", + description = "Swap Quick-Pay(100) at the Hardwood Grove", + section = objectSection + ) + default boolean swapHardWoodGrove() + { + return true; + } + + @ConfigItem( + keyName = "swapHardWoodGroveParcel", + name = "Hardwood Grove Send-Parcel", + description = "Swap Send-Parcel at the Hardwood Grove", + section = npcSection + ) + default boolean swapHardWoodGroveParcel() + { + return true; + } + + @ConfigItem( + keyName = "swapHarpoon", + name = "Harpoon", + description = "Swap Cage, Big Net with Harpoon on Fishing spot", + section = objectSection + ) + default boolean swapHarpoon() + { + return false; + } + + @ConfigItem( + keyName = "swapHelp", + name = "Help", + description = "Swap Talk-to with Help on Arceuus library customers", + section = npcSection + ) + default boolean swapHelp() + { + return true; + } + + @ConfigItem( + keyName = "swapHomePortal", + name = "Home", + description = "Swap Enter with Home or Build or Friend's house on Portal", + section = objectSection + ) + default HouseMode swapHomePortal() + { + return HouseMode.HOME; + } + + @ConfigItem( + keyName = "swapHouseAdvertisement", + name = "House Advertisement", + description = "Swap View with Add-House or Visit-Last on House Advertisement board", + section = objectSection + ) + default HouseAdvertisementMode swapHouseAdvertisement() + { + return HouseAdvertisementMode.VIEW; + } + + @ConfigItem( + keyName = "swapPay", + name = "Pay", + description = "Swap Talk-to with Pay on NPC
Example: Elstan, Heskel, Fayeth", + section = npcSection + ) + default boolean swapPay() + { + return true; + } + + @ConfigItem( + keyName = "swapJewelleryBox", + name = "Jewellery Box", + description = "Swap Teleport Menu with previous destination on Jewellery Box", + section = objectSection + ) + default boolean swapJewelleryBox() + { + return false; + } + + @ConfigItem( + keyName = "swapPrivate", + name = "Private", + description = "Swap Shared with Private on the Chambers of Xeric storage units.", + section = objectSection + ) + default boolean swapPrivate() + { + return false; + } + + @ConfigItem( + keyName = "swapPick", + name = "Pick", + description = "Swap Pick with Pick-lots of the Gourd tree in the Chambers of Xeric", + section = objectSection + ) + default boolean swapPick() + { + return false; + } + + @ConfigItem( + keyName = "swapQuick", + name = "Quick Pass/Open/Start/Travel", + description = "Swap Pass with Quick-Pass, Open with Quick-Open, Ring with Quick-Start and Talk-to with Quick-Travel", + section = objectSection + ) + default boolean swapQuick() + { + return true; + } + + @ConfigItem( + keyName = "swapBoxTrap", + name = "Reset", + description = "Swap Check with Reset on box trap", + section = objectSection + ) + default boolean swapBoxTrap() + { + return true; + } + + @ConfigItem( + keyName = "swapTeleportItem", + name = "Teleport item", + description = "Swap Wear, Wield with Rub, Teleport on teleport item
Example: Amulet of glory, Explorer's ring, Chronicle", + section = itemSection + ) + default boolean swapTeleportItem() + { + return false; + } + + @ConfigItem( + keyName = "swapAbyssTeleport", + name = "Teleport to Abyss", + description = "Swap Talk-to with Teleport for the Mage of Zamorak", + section = npcSection + ) + default boolean swapAbyssTeleport() + { + return true; + } + + @ConfigItem( + keyName = "swapTrade", + name = "Trade", + description = "Swap Talk-to with Trade on NPC
Example: Shop keeper, Shop assistant", + section = npcSection + ) + default boolean swapTrade() + { + return true; + } + + @ConfigItem( + keyName = "swapTravel", + name = "Travel", + description = "Swap Talk-to with Travel, Take-boat, Pay-fare, Charter on NPC
Example: Squire, Monk of Entrana, Customs officer, Trader Crewmember", + section = npcSection + ) + default boolean swapTravel() + { + return true; + } + + @ConfigItem( + keyName = "swapEnchant", + name = "Enchant", + description = "Swap Talk-to with Enchant for Eluned", + section = npcSection + ) + default boolean swapEnchant() + { + return true; + } + + @ConfigItem( + keyName = "swapTeleportSpell", + name = "Shift-click teleport spells", + description = "Swap teleport spells that have a second destination on shift", + section = uiSection + ) + default boolean swapTeleportSpell() + { + return false; + } + + @ConfigItem( + keyName = "swapStartMinigame", + name = "Pyramid Plunder Start-minigame", + description = "Swap Talk-to with Start-minigame at the Guardian Mummy", + section = npcSection + ) + default boolean swapStartMinigame() + { + return true; + } + + @ConfigItem( + keyName = "swapQuickleave", + name = "Quick-Leave", + description = "Swap Leave Tomb with Quick-Leave at Pyramid Plunder", + section = objectSection + ) + default boolean swapQuickLeave() + { + return false; + } + + @ConfigItem( + keyName = "swapGEItemCollect", + name = "GE Item Collect", + description = "Swap Collect-notes, Collect-items, or Bank options from GE offer", + section = uiSection + ) + default GEItemCollectMode swapGEItemCollect() + { + return GEItemCollectMode.DEFAULT; + } + + @ConfigItem( + keyName = "swapGEAbort", + name = "GE Abort", + description = "Swap abort offer on Grand Exchange offers when shift-clicking" + , + section = uiSection + ) + default boolean swapGEAbort() + { + return false; + } + + @ConfigItem( + keyName = "swapNpcContact", + name = "NPC Contact", + description = "Swap NPC Contact with last contacted NPC when shift-clicking", + section = uiSection + ) + default boolean swapNpcContact() + { + return false; + } + + @ConfigItem( + keyName = "bankWithdrawShiftClick", + name = "Bank Withdraw Shift-Click", + description = "Swaps the behavior of shift-click when withdrawing from bank.", + section = itemSection + ) + default ShiftWithdrawMode bankWithdrawShiftClick() + { + return ShiftWithdrawMode.OFF; + } + + @ConfigItem( + keyName = "bankDepositShiftClick", + name = "Bank Deposit Shift-Click", + description = "Swaps the behavior of shift-click when depositing to bank.", + section = itemSection + ) + default ShiftDepositMode bankDepositShiftClick() + { + return ShiftDepositMode.OFF; + } + + @ConfigItem( + keyName = "shopBuy", + name = "Shop Buy Shift-Click", + description = "Swaps the Buy options with Value on items in shops when shift is held.", + section = uiSection + ) + default BuyMode shopBuy() + { + return BuyMode.OFF; + } + + @ConfigItem( + keyName = "shopSell", + name = "Shop Sell Shift-Click", + description = "Swaps the Sell options with Value on items in your inventory when selling to shops when shift is held.", + section = uiSection + ) + default SellMode shopSell() + { + return SellMode.OFF; + } + + @ConfigItem( + keyName = "swapEssenceMineTeleport", + name = "Essence Mine Teleport", + description = "Swaps Talk-To with Teleport for NPCs which teleport you to the essence mine", + section = npcSection + ) + default boolean swapEssenceMineTeleport() + { + return false; + } + + @ConfigItem( + keyName = "swapNets", + name = "Nets", + description = "Swap Talk-to with Nets on Annette", + section = npcSection + ) + default boolean swapNets() + { + return true; + } + + @ConfigItem( + keyName = "swapGauntlet", + name = "Corrupted Gauntlet", + description = "Swap Enter with Enter-corrupted when entering The Gauntlet", + section = objectSection + ) + default boolean swapGauntlet() + { + return false; + } + + @ConfigItem( + keyName = "swapTan", + name = "Tan", + description = "Swap Tan 1 with Tan All", + section = uiSection + ) + default boolean swapTan() + { + return false; + } + + @ConfigItem( + keyName = "swapCollectMiscellania", + name = "Miscellania", + description = "Swap Talk-to with Collect for Advisor Ghrim", + section = npcSection + ) + default boolean swapCollectMiscellania() + { + return false; + } + + @ConfigItem( + keyName = "swapDepositItems", + name = "Deposit Items", + description = "Swap Talk-to with Deposit-items", + section = npcSection + ) + default boolean swapDepositItems() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java new file mode 100644 index 0000000000..fc868c6de4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java @@ -0,0 +1,851 @@ +/* + * Copyright (c) 2018, Adam + * Copyright (c) 2018, Kamiel + * Copyright (c) 2019, Rami + * 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.menuentryswapper; + +import com.google.common.annotations.VisibleForTesting; +import static com.google.common.base.Predicates.alwaysTrue; +import static com.google.common.base.Predicates.equalTo; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.inject.Provides; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.inject.Inject; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.ItemComposition; +import net.runelite.api.KeyCode; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.NPC; +import net.runelite.api.events.ClientTick; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.events.MenuOpened; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.events.PostItemComposition; +import net.runelite.api.events.WidgetMenuOptionClicked; +import net.runelite.api.widgets.WidgetID; +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.game.ItemManager; +import net.runelite.client.game.ItemVariationMapping; +import net.runelite.client.menus.MenuManager; +import net.runelite.client.menus.WidgetMenuOption; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.Text; +import org.apache.commons.lang3.ArrayUtils; + +@PluginDescriptor( + name = "Menu Entry Swapper", + description = "Change the default option that is displayed when hovering over objects", + tags = {"npcs", "inventory", "items", "objects"}, + enabledByDefault = false +) +public class MenuEntrySwapperPlugin extends Plugin +{ + private static final String CONFIGURE = "Configure"; + private static final String SAVE = "Save"; + private static final String RESET = "Reset"; + private static final String MENU_TARGET = "Shift-click"; + + private static final String SHIFTCLICK_CONFIG_GROUP = "shiftclick"; + private static final String ITEM_KEY_PREFIX = "item_"; + + private static final WidgetMenuOption FIXED_INVENTORY_TAB_CONFIGURE = new WidgetMenuOption(CONFIGURE, + MENU_TARGET, WidgetInfo.FIXED_VIEWPORT_INVENTORY_TAB); + + private static final WidgetMenuOption FIXED_INVENTORY_TAB_SAVE = new WidgetMenuOption(SAVE, + MENU_TARGET, WidgetInfo.FIXED_VIEWPORT_INVENTORY_TAB); + + private static final WidgetMenuOption RESIZABLE_INVENTORY_TAB_CONFIGURE = new WidgetMenuOption(CONFIGURE, + MENU_TARGET, WidgetInfo.RESIZABLE_VIEWPORT_INVENTORY_TAB); + + private static final WidgetMenuOption RESIZABLE_INVENTORY_TAB_SAVE = new WidgetMenuOption(SAVE, + MENU_TARGET, WidgetInfo.RESIZABLE_VIEWPORT_INVENTORY_TAB); + + private static final WidgetMenuOption RESIZABLE_BOTTOM_LINE_INVENTORY_TAB_CONFIGURE = new WidgetMenuOption(CONFIGURE, + MENU_TARGET, WidgetInfo.RESIZABLE_VIEWPORT_BOTTOM_LINE_INVENTORY_TAB); + + private static final WidgetMenuOption RESIZABLE_BOTTOM_LINE_INVENTORY_TAB_SAVE = new WidgetMenuOption(SAVE, + MENU_TARGET, WidgetInfo.RESIZABLE_VIEWPORT_BOTTOM_LINE_INVENTORY_TAB); + + private static final Set NPC_MENU_TYPES = ImmutableSet.of( + MenuAction.NPC_FIRST_OPTION, + MenuAction.NPC_SECOND_OPTION, + MenuAction.NPC_THIRD_OPTION, + MenuAction.NPC_FOURTH_OPTION, + MenuAction.NPC_FIFTH_OPTION, + MenuAction.EXAMINE_NPC); + + private static final Set ESSENCE_MINE_NPCS = ImmutableSet.of( + "aubury", + "wizard sedridor", + "wizard distentor", + "wizard cromperty", + "brimstail" + ); + + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Inject + private MenuEntrySwapperConfig config; + + @Inject + private ConfigManager configManager; + + @Inject + private MenuManager menuManager; + + @Inject + private ItemManager itemManager; + + @Getter + private boolean configuringShiftClick = false; + + private final Multimap swaps = LinkedHashMultimap.create(); + private final ArrayListMultimap optionIndexes = ArrayListMultimap.create(); + + @Provides + MenuEntrySwapperConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(MenuEntrySwapperConfig.class); + } + + @Override + public void startUp() + { + if (config.shiftClickCustomization()) + { + enableCustomization(); + } + + setupSwaps(); + } + + @Override + public void shutDown() + { + disableCustomization(); + + swaps.clear(); + } + + @VisibleForTesting + void setupSwaps() + { + swap("talk-to", "mage of zamorak", "teleport", config::swapAbyssTeleport); + swap("talk-to", "rionasta", "send-parcel", config::swapHardWoodGroveParcel); + swap("talk-to", "captain khaled", "task", config::swapCaptainKhaled); + swap("talk-to", "bank", config::swapBank); + swap("talk-to", "contract", config::swapContract); + swap("talk-to", "exchange", config::swapExchange); + swap("talk-to", "help", config::swapHelp); + swap("talk-to", "nets", config::swapNets); + swap("talk-to", "repairs", config::swapDarkMage); + // make sure assignment swap is higher priority than trade swap for slayer masters + swap("talk-to", "assignment", config::swapAssignment); + swap("talk-to", "trade", config::swapTrade); + swap("talk-to", "trade-with", config::swapTrade); + swap("talk-to", "shop", config::swapTrade); + swap("talk-to", "robin", "claim-slime", config::claimSlime); + swap("talk-to", "travel", config::swapTravel); + swap("talk-to", "pay-fare", config::swapTravel); + swap("talk-to", "charter", config::swapTravel); + swap("talk-to", "take-boat", config::swapTravel); + swap("talk-to", "fly", config::swapTravel); + swap("talk-to", "jatizso", config::swapTravel); + swap("talk-to", "neitiznot", config::swapTravel); + swap("talk-to", "rellekka", config::swapTravel); + swap("talk-to", "ungael", config::swapTravel); + swap("talk-to", "pirate's cove", config::swapTravel); + swap("talk-to", "waterbirth island", config::swapTravel); + swap("talk-to", "island of stone", config::swapTravel); + swap("talk-to", "miscellania", config::swapTravel); + swap("talk-to", "follow", config::swapTravel); + swap("talk-to", "transport", config::swapTravel); + swap("talk-to", "pay", config::swapPay); + swapContains("talk-to", alwaysTrue(), "pay (", config::swapPay); + swap("talk-to", "decant", config::swapDecant); + swap("talk-to", "quick-travel", config::swapQuick); + swap("talk-to", "enchant", config::swapEnchant); + swap("talk-to", "start-minigame", config::swapStartMinigame); + swap("talk-to", ESSENCE_MINE_NPCS::contains, "teleport", config::swapEssenceMineTeleport); + swap("talk-to", "collect", config::swapCollectMiscellania); + swap("talk-to", "deposit-items", config::swapDepositItems); + + swap("leave tomb", "quick-leave", config::swapQuickLeave); + swap("tomb door", "quick-leave", config::swapQuickLeave); + + swap("pass", "energy barrier", "pay-toll(2-ecto)", config::swapTravel); + swap("open", "gate", "pay-toll(10gp)", config::swapTravel); + + swap("open", "hardwood grove doors", "quick-pay(100)", config::swapHardWoodGrove); + + swap("inspect", "trapdoor", "travel", config::swapTravel); + swap("board", "travel cart", "pay-fare", config::swapTravel); + + swap("cage", "harpoon", config::swapHarpoon); + swap("big net", "harpoon", config::swapHarpoon); + swap("net", "harpoon", config::swapHarpoon); + + swap("enter", "portal", "home", () -> config.swapHomePortal() == HouseMode.HOME); + swap("enter", "portal", "build mode", () -> config.swapHomePortal() == HouseMode.BUILD_MODE); + swap("enter", "portal", "friend's house", () -> config.swapHomePortal() == HouseMode.FRIENDS_HOUSE); + + swap("view", "add-house", () -> config.swapHouseAdvertisement() == HouseAdvertisementMode.ADD_HOUSE); + swap("view", "visit-last", () -> config.swapHouseAdvertisement() == HouseAdvertisementMode.VISIT_LAST); + + for (String option : new String[]{"zanaris", "tree"}) + { + swapContains(option, alwaysTrue(), "last-destination", () -> config.swapFairyRing() == FairyRingMode.LAST_DESTINATION); + swapContains(option, alwaysTrue(), "configure", () -> config.swapFairyRing() == FairyRingMode.CONFIGURE); + } + + swapContains("configure", alwaysTrue(), "last-destination", () -> + config.swapFairyRing() == FairyRingMode.LAST_DESTINATION || config.swapFairyRing() == FairyRingMode.ZANARIS); + swapContains("tree", alwaysTrue(), "zanaris", () -> config.swapFairyRing() == FairyRingMode.ZANARIS); + + swap("check", "reset", config::swapBoxTrap); + swap("dismantle", "reset", config::swapBoxTrap); + swap("take", "lay", config::swapBoxTrap); + + swap("pick-up", "chase", config::swapChase); + + swap("interact", target -> target.endsWith("birdhouse"), "empty", config::swapBirdhouseEmpty); + + swap("enter", "the gauntlet", "enter-corrupted", config::swapGauntlet); + + swap("enter", "quick-enter", config::swapQuick); + swap("ring", "quick-start", config::swapQuick); + swap("pass", "quick-pass", config::swapQuick); + swap("pass", "quick pass", config::swapQuick); + swap("open", "quick-open", config::swapQuick); + swap("climb-down", "quick-start", config::swapQuick); + swap("climb-down", "pay", config::swapQuick); + + swap("admire", "teleport", config::swapAdmire); + swap("admire", "spellbook", config::swapAdmire); + swap("admire", "perks", config::swapAdmire); + + swap("teleport menu", "duel arena", config::swapJewelleryBox); + swap("teleport menu", "castle wars", config::swapJewelleryBox); + swap("teleport menu", "ferox enclave", config::swapJewelleryBox); + swap("teleport menu", "burthorpe", config::swapJewelleryBox); + swap("teleport menu", "barbarian outpost", config::swapJewelleryBox); + swap("teleport menu", "corporeal beast", config::swapJewelleryBox); + swap("teleport menu", "tears of guthix", config::swapJewelleryBox); + swap("teleport menu", "wintertodt camp", config::swapJewelleryBox); + swap("teleport menu", "warriors' guild", config::swapJewelleryBox); + swap("teleport menu", "champions' guild", config::swapJewelleryBox); + swap("teleport menu", "monastery", config::swapJewelleryBox); + swap("teleport menu", "ranging guild", config::swapJewelleryBox); + swap("teleport menu", "fishing guild", config::swapJewelleryBox); + swap("teleport menu", "mining guild", config::swapJewelleryBox); + swap("teleport menu", "crafting guild", config::swapJewelleryBox); + swap("teleport menu", "cooking guild", config::swapJewelleryBox); + swap("teleport menu", "woodcutting guild", config::swapJewelleryBox); + swap("teleport menu", "farming guild", config::swapJewelleryBox); + swap("teleport menu", "miscellania", config::swapJewelleryBox); + swap("teleport menu", "grand exchange", config::swapJewelleryBox); + swap("teleport menu", "falador park", config::swapJewelleryBox); + swap("teleport menu", "dondakan's rock", config::swapJewelleryBox); + swap("teleport menu", "edgeville", config::swapJewelleryBox); + swap("teleport menu", "karamja", config::swapJewelleryBox); + swap("teleport menu", "draynor village", config::swapJewelleryBox); + swap("teleport menu", "al kharid", config::swapJewelleryBox); + + swap("shared", "private", config::swapPrivate); + + swap("pick", "pick-lots", config::swapPick); + + swap("view offer", "abort offer", () -> shiftModifier() && config.swapGEAbort()); + + Arrays.asList( + "honest jimmy", "bert the sandman", "advisor ghrim", "dark mage", "lanthus", "turael", "mazchna", "vannaka", + "chaeldar", "nieve", "steve", "duradel", "krystilia", "konar", "murphy", "cyrisus", "smoggy", "ginea", "watson", + "barbarian guard", "amy", "random" + ).forEach(npc -> swap("cast", "npc contact", npc, () -> shiftModifier() && config.swapNpcContact())); + + swap("value", "buy 1", () -> shiftModifier() && config.shopBuy() == BuyMode.BUY_1); + swap("value", "buy 5", () -> shiftModifier() && config.shopBuy() == BuyMode.BUY_5); + swap("value", "buy 10", () -> shiftModifier() && config.shopBuy() == BuyMode.BUY_10); + swap("value", "buy 50", () -> shiftModifier() && config.shopBuy() == BuyMode.BUY_50); + + swap("value", "sell 1", () -> shiftModifier() && config.shopSell() == SellMode.SELL_1); + swap("value", "sell 5", () -> shiftModifier() && config.shopSell() == SellMode.SELL_5); + swap("value", "sell 10", () -> shiftModifier() && config.shopSell() == SellMode.SELL_10); + swap("value", "sell 50", () -> shiftModifier() && config.shopSell() == SellMode.SELL_50); + + swap("wear", "rub", config::swapTeleportItem); + swap("wear", "teleport", config::swapTeleportItem); + swap("wield", "teleport", config::swapTeleportItem); + + swap("bury", "use", config::swapBones); + + swap("clean", "use", config::swapHerbs); + + swap("read", "recite-prayer", config::swapPrayerBook); + + swap("collect-note", "collect-item", () -> config.swapGEItemCollect() == GEItemCollectMode.ITEMS); + swap("collect-notes", "collect-items", () -> config.swapGEItemCollect() == GEItemCollectMode.ITEMS); + + swap("collect-item", "collect-note", () -> config.swapGEItemCollect() == GEItemCollectMode.NOTES); + swap("collect-items", "collect-notes", () -> config.swapGEItemCollect() == GEItemCollectMode.NOTES); + + swap("collect to inventory", "collect to bank", () -> config.swapGEItemCollect() == GEItemCollectMode.BANK); + swap("collect", "bank", () -> config.swapGEItemCollect() == GEItemCollectMode.BANK); + swap("collect-note", "bank", () -> config.swapGEItemCollect() == GEItemCollectMode.BANK); + swap("collect-notes", "bank", () -> config.swapGEItemCollect() == GEItemCollectMode.BANK); + swap("collect-item", "bank", () -> config.swapGEItemCollect() == GEItemCollectMode.BANK); + swap("collect-items", "bank", () -> config.swapGEItemCollect() == GEItemCollectMode.BANK); + + swap("tan 1", "tan all", config::swapTan); + + swapTeleport("varrock teleport", "grand exchange"); + swapTeleport("camelot teleport", "seers'"); + swapTeleport("watchtower teleport", "yanille"); + swapTeleport("teleport to house", "outside"); + } + + private void swap(String option, String swappedOption, Supplier enabled) + { + swap(option, alwaysTrue(), swappedOption, enabled); + } + + private void swap(String option, String target, String swappedOption, Supplier enabled) + { + swap(option, equalTo(target), swappedOption, enabled); + } + + private void swap(String option, Predicate targetPredicate, String swappedOption, Supplier enabled) + { + swaps.put(option, new Swap(alwaysTrue(), targetPredicate, swappedOption, enabled, true)); + } + + private void swapContains(String option, Predicate targetPredicate, String swappedOption, Supplier enabled) + { + swaps.put(option, new Swap(alwaysTrue(), targetPredicate, swappedOption, enabled, false)); + } + + private void swapTeleport(String option, String swappedOption) + { + swap("cast", option, swappedOption, () -> shiftModifier() && config.swapTeleportSpell()); + swap(swappedOption, option, "cast", () -> shiftModifier() && config.swapTeleportSpell()); + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals(MenuEntrySwapperConfig.GROUP) && event.getKey().equals("shiftClickCustomization")) + { + if (config.shiftClickCustomization()) + { + enableCustomization(); + } + else + { + disableCustomization(); + } + } + else if (event.getGroup().equals(SHIFTCLICK_CONFIG_GROUP) && event.getKey().startsWith(ITEM_KEY_PREFIX)) + { + clientThread.invoke(this::resetItemCompositionCache); + } + } + + private void resetItemCompositionCache() + { + itemManager.invalidateItemCompositionCache(); + client.getItemCompositionCache().reset(); + } + + private Integer getSwapConfig(int itemId) + { + itemId = ItemVariationMapping.map(itemId); + String config = configManager.getConfiguration(SHIFTCLICK_CONFIG_GROUP, ITEM_KEY_PREFIX + itemId); + if (config == null || config.isEmpty()) + { + return null; + } + + return Integer.parseInt(config); + } + + private void setSwapConfig(int itemId, int index) + { + itemId = ItemVariationMapping.map(itemId); + configManager.setConfiguration(SHIFTCLICK_CONFIG_GROUP, ITEM_KEY_PREFIX + itemId, index); + } + + private void unsetSwapConfig(int itemId) + { + itemId = ItemVariationMapping.map(itemId); + configManager.unsetConfiguration(SHIFTCLICK_CONFIG_GROUP, ITEM_KEY_PREFIX + itemId); + } + + private void enableCustomization() + { + refreshShiftClickCustomizationMenus(); + // set shift click action index on the item compositions + clientThread.invoke(this::resetItemCompositionCache); + } + + private void disableCustomization() + { + removeShiftClickCustomizationMenus(); + configuringShiftClick = false; + // flush item compositions to reset the shift click action index + clientThread.invoke(this::resetItemCompositionCache); + } + + @Subscribe + public void onWidgetMenuOptionClicked(WidgetMenuOptionClicked event) + { + if (event.getWidget() == WidgetInfo.FIXED_VIEWPORT_INVENTORY_TAB + || event.getWidget() == WidgetInfo.RESIZABLE_VIEWPORT_INVENTORY_TAB + || event.getWidget() == WidgetInfo.RESIZABLE_VIEWPORT_BOTTOM_LINE_INVENTORY_TAB) + { + configuringShiftClick = event.getMenuOption().equals(CONFIGURE) && Text.removeTags(event.getMenuTarget()).equals(MENU_TARGET); + refreshShiftClickCustomizationMenus(); + } + } + + @Subscribe + public void onMenuOpened(MenuOpened event) + { + if (!configuringShiftClick) + { + return; + } + + MenuEntry firstEntry = event.getFirstEntry(); + if (firstEntry == null) + { + return; + } + + int widgetId = firstEntry.getParam1(); + if (widgetId != WidgetInfo.INVENTORY.getId()) + { + return; + } + + int itemId = firstEntry.getIdentifier(); + if (itemId == -1) + { + return; + } + + ItemComposition itemComposition = client.getItemDefinition(itemId); + String itemName = itemComposition.getName(); + String option = "Use"; + int shiftClickActionIndex = itemComposition.getShiftClickActionIndex(); + String[] inventoryActions = itemComposition.getInventoryActions(); + + if (shiftClickActionIndex >= 0 && shiftClickActionIndex < inventoryActions.length) + { + option = inventoryActions[shiftClickActionIndex]; + } + + MenuEntry[] entries = event.getMenuEntries(); + + for (MenuEntry entry : entries) + { + if (itemName.equals(Text.removeTags(entry.getTarget()))) + { + entry.setType(MenuAction.RUNELITE.getId()); + + if (option.equals(entry.getOption())) + { + entry.setOption("* " + option); + } + } + } + + final MenuEntry resetShiftClickEntry = new MenuEntry(); + resetShiftClickEntry.setOption(RESET); + resetShiftClickEntry.setTarget(MENU_TARGET); + resetShiftClickEntry.setIdentifier(itemId); + resetShiftClickEntry.setParam1(widgetId); + resetShiftClickEntry.setType(MenuAction.RUNELITE.getId()); + client.setMenuEntries(ArrayUtils.addAll(entries, resetShiftClickEntry)); + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded menuEntryAdded) + { + // This swap needs to happen prior to drag start on click, which happens during + // widget ticking and prior to our client tick event. This is because drag start + // is what builds the context menu row which is what the eventual click will use + + // Swap to shift-click deposit behavior + // Deposit- op 1 is the current withdraw amount 1/5/10/x for deposit box interface and chambers of xeric storage unit. + // Deposit- op 2 is the current withdraw amount 1/5/10/x for bank interface + if (shiftModifier() && config.bankDepositShiftClick() != ShiftDepositMode.OFF + && menuEntryAdded.getType() == MenuAction.CC_OP.getId() + && (menuEntryAdded.getIdentifier() == 2 || menuEntryAdded.getIdentifier() == 1) + && (menuEntryAdded.getOption().startsWith("Deposit-") || menuEntryAdded.getOption().startsWith("Store") || menuEntryAdded.getOption().startsWith("Donate"))) + { + ShiftDepositMode shiftDepositMode = config.bankDepositShiftClick(); + final int widgetGroupId = WidgetInfo.TO_GROUP(menuEntryAdded.getActionParam1()); + final int opId = widgetGroupId == WidgetID.DEPOSIT_BOX_GROUP_ID ? shiftDepositMode.getIdentifierDepositBox() + : widgetGroupId == WidgetID.CHAMBERS_OF_XERIC_STORAGE_UNIT_INVENTORY_GROUP_ID ? shiftDepositMode.getIdentifierChambersStorageUnit() + : shiftDepositMode.getIdentifier(); + final int actionId = opId >= 6 ? MenuAction.CC_OP_LOW_PRIORITY.getId() : MenuAction.CC_OP.getId(); + bankModeSwap(actionId, opId); + } + + // Swap to shift-click withdraw behavior + // Deposit- op 1 is the current withdraw amount 1/5/10/x + if (shiftModifier() && config.bankWithdrawShiftClick() != ShiftWithdrawMode.OFF + && menuEntryAdded.getType() == MenuAction.CC_OP.getId() && menuEntryAdded.getIdentifier() == 1 + && menuEntryAdded.getOption().startsWith("Withdraw")) + { + ShiftWithdrawMode shiftWithdrawMode = config.bankWithdrawShiftClick(); + final int widgetGroupId = WidgetInfo.TO_GROUP(menuEntryAdded.getActionParam1()); + final int actionId, opId; + if (widgetGroupId == WidgetID.CHAMBERS_OF_XERIC_STORAGE_UNIT_PRIVATE_GROUP_ID || widgetGroupId == WidgetID.CHAMBERS_OF_XERIC_STORAGE_UNIT_SHARED_GROUP_ID) + { + actionId = MenuAction.CC_OP.getId(); + opId = shiftWithdrawMode.getIdentifierChambersStorageUnit(); + } + else + { + actionId = shiftWithdrawMode.getMenuAction().getId(); + opId = shiftWithdrawMode.getIdentifier(); + } + bankModeSwap(actionId, opId); + } + } + + private void bankModeSwap(int entryTypeId, int entryIdentifier) + { + MenuEntry[] menuEntries = client.getMenuEntries(); + + for (int i = menuEntries.length - 1; i >= 0; --i) + { + MenuEntry entry = menuEntries[i]; + + if (entry.getType() == entryTypeId && entry.getIdentifier() == entryIdentifier) + { + // Raise the priority of the op so it doesn't get sorted later + entry.setType(MenuAction.CC_OP.getId()); + + menuEntries[i] = menuEntries[menuEntries.length - 1]; + menuEntries[menuEntries.length - 1] = entry; + + client.setMenuEntries(menuEntries); + break; + } + } + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked event) + { + if (event.getMenuAction() != MenuAction.RUNELITE || event.getWidgetId() != WidgetInfo.INVENTORY.getId()) + { + return; + } + + int itemId = event.getId(); + + if (itemId == -1) + { + return; + } + + String option = event.getMenuOption(); + String target = event.getMenuTarget(); + ItemComposition itemComposition = client.getItemDefinition(itemId); + + if (option.equals(RESET) && target.equals(MENU_TARGET)) + { + unsetSwapConfig(itemId); + return; + } + + if (!itemComposition.getName().equals(Text.removeTags(target))) + { + return; + } + + if (option.equals("Use")) //because "Use" is not in inventoryActions + { + setSwapConfig(itemId, -1); + } + else + { + String[] inventoryActions = itemComposition.getInventoryActions(); + + for (int index = 0; index < inventoryActions.length; index++) + { + if (option.equals(inventoryActions[index])) + { + setSwapConfig(itemId, index); + break; + } + } + } + } + + private void swapMenuEntry(int index, MenuEntry menuEntry) + { + final int eventId = menuEntry.getIdentifier(); + final MenuAction menuAction = MenuAction.of(menuEntry.getType()); + final String option = Text.removeTags(menuEntry.getOption()).toLowerCase(); + final String target = Text.removeTags(menuEntry.getTarget()).toLowerCase(); + final NPC hintArrowNpc = client.getHintArrowNpc(); + + if (hintArrowNpc != null + && hintArrowNpc.getIndex() == eventId + && NPC_MENU_TYPES.contains(menuAction)) + { + return; + } + + if (shiftModifier() && (menuAction == MenuAction.ITEM_FIRST_OPTION + || menuAction == MenuAction.ITEM_SECOND_OPTION + || menuAction == MenuAction.ITEM_THIRD_OPTION + || menuAction == MenuAction.ITEM_FOURTH_OPTION + || menuAction == MenuAction.ITEM_FIFTH_OPTION + || menuAction == MenuAction.ITEM_USE)) + { + // Special case use shift click due to items not actually containing a "Use" option, making + // the client unable to perform the swap itself. + if (config.shiftClickCustomization() && !option.equals("use")) + { + Integer customOption = getSwapConfig(eventId); + + if (customOption != null && customOption == -1) + { + swap("use", target, index, true); + } + } + + // don't perform swaps on items when shift is held; instead prefer the client menu swap, which + // we may have overwrote + return; + } + + Collection swaps = this.swaps.get(option); + for (Swap swap : swaps) + { + if (swap.getTargetPredicate().test(target) && swap.getEnabled().get()) + { + if (swap(swap.getSwappedOption(), target, index, swap.isStrict())) + { + break; + } + } + } + } + + @Subscribe + public void onClientTick(ClientTick clientTick) + { + // The menu is not rebuilt when it is open, so don't swap or else it will + // repeatedly swap entries + if (client.getGameState() != GameState.LOGGED_IN || client.isMenuOpen()) + { + return; + } + + MenuEntry[] menuEntries = client.getMenuEntries(); + + // Build option map for quick lookup in findIndex + int idx = 0; + optionIndexes.clear(); + for (MenuEntry entry : menuEntries) + { + String option = Text.removeTags(entry.getOption()).toLowerCase(); + optionIndexes.put(option, idx++); + } + + // Perform swaps + idx = 0; + for (MenuEntry entry : menuEntries) + { + swapMenuEntry(idx++, entry); + } + } + + @Subscribe + public void onPostItemComposition(PostItemComposition event) + { + if (!config.shiftClickCustomization()) + { + // since shift-click is done by the client we have to check if our shift click customization is on + // prior to altering the item shift click action index. + return; + } + + ItemComposition itemComposition = event.getItemComposition(); + Integer option = getSwapConfig(itemComposition.getId()); + + if (option != null) + { + itemComposition.setShiftClickActionIndex(option); + } + } + + private boolean swap(String option, String target, int index, boolean strict) + { + MenuEntry[] menuEntries = client.getMenuEntries(); + + // find option to swap with + int optionIdx = findIndex(menuEntries, index, option, target, strict); + + if (optionIdx >= 0) + { + swap(optionIndexes, menuEntries, optionIdx, index); + return true; + } + + return false; + } + + private int findIndex(MenuEntry[] entries, int limit, String option, String target, boolean strict) + { + if (strict) + { + List indexes = optionIndexes.get(option); + + // We want the last index which matches the target, as that is what is top-most + // on the menu + for (int i = indexes.size() - 1; i >= 0; --i) + { + int idx = indexes.get(i); + MenuEntry entry = entries[idx]; + String entryTarget = Text.removeTags(entry.getTarget()).toLowerCase(); + + // Limit to the last index which is prior to the current entry + if (idx < limit && entryTarget.equals(target)) + { + return idx; + } + } + } + else + { + // Without strict matching we have to iterate all entries up to the current limit... + for (int i = limit - 1; i >= 0; i--) + { + MenuEntry entry = entries[i]; + String entryOption = Text.removeTags(entry.getOption()).toLowerCase(); + String entryTarget = Text.removeTags(entry.getTarget()).toLowerCase(); + + if (entryOption.contains(option.toLowerCase()) && entryTarget.equals(target)) + { + return i; + } + } + + } + + return -1; + } + + private void swap(ArrayListMultimap optionIndexes, MenuEntry[] entries, int index1, int index2) + { + MenuEntry entry1 = entries[index1], + entry2 = entries[index2]; + + entries[index1] = entry2; + entries[index2] = entry1; + + client.setMenuEntries(entries); + + // Update optionIndexes + String option1 = Text.removeTags(entry1.getOption()).toLowerCase(), + option2 = Text.removeTags(entry2.getOption()).toLowerCase(); + + List list1 = optionIndexes.get(option1), + list2 = optionIndexes.get(option2); + + // call remove(Object) instead of remove(int) + list1.remove((Integer) index1); + list2.remove((Integer) index2); + + sortedInsert(list1, index2); + sortedInsert(list2, index1); + } + + private static > void sortedInsert(List list, T value) // NOPMD: UnusedPrivateMethod: false positive + { + int idx = Collections.binarySearch(list, value); + list.add(idx < 0 ? -idx - 1 : idx, value); + } + + private void removeShiftClickCustomizationMenus() + { + menuManager.removeManagedCustomMenu(FIXED_INVENTORY_TAB_CONFIGURE); + menuManager.removeManagedCustomMenu(FIXED_INVENTORY_TAB_SAVE); + menuManager.removeManagedCustomMenu(RESIZABLE_BOTTOM_LINE_INVENTORY_TAB_CONFIGURE); + menuManager.removeManagedCustomMenu(RESIZABLE_BOTTOM_LINE_INVENTORY_TAB_SAVE); + menuManager.removeManagedCustomMenu(RESIZABLE_INVENTORY_TAB_CONFIGURE); + menuManager.removeManagedCustomMenu(RESIZABLE_INVENTORY_TAB_SAVE); + } + + private void refreshShiftClickCustomizationMenus() + { + removeShiftClickCustomizationMenus(); + if (configuringShiftClick) + { + menuManager.addManagedCustomMenu(FIXED_INVENTORY_TAB_SAVE); + menuManager.addManagedCustomMenu(RESIZABLE_BOTTOM_LINE_INVENTORY_TAB_SAVE); + menuManager.addManagedCustomMenu(RESIZABLE_INVENTORY_TAB_SAVE); + } + else + { + menuManager.addManagedCustomMenu(FIXED_INVENTORY_TAB_CONFIGURE); + menuManager.addManagedCustomMenu(RESIZABLE_BOTTOM_LINE_INVENTORY_TAB_CONFIGURE); + menuManager.addManagedCustomMenu(RESIZABLE_INVENTORY_TAB_CONFIGURE); + } + } + + private boolean shiftModifier() + { + return client.isKeyPressed(KeyCode.KC_SHIFT); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/SellMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/SellMode.java new file mode 100644 index 0000000000..048b77752e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/SellMode.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020, Sam + * 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.menuentryswapper; + +public enum SellMode +{ + OFF, + SELL_1, + SELL_5, + SELL_10, + SELL_50; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftDepositMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftDepositMode.java new file mode 100644 index 0000000000..add9d4afe8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftDepositMode.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020, Zach + * 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.menuentryswapper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ShiftDepositMode +{ + DEPOSIT_1("Deposit-1", 3, 2, 1), + DEPOSIT_5("Deposit-5", 4, 3, 2), + DEPOSIT_10("Deposit-10", 5, 4, 3), + DEPOSIT_X("Deposit-X", 6, 6, 5), + DEPOSIT_ALL("Deposit-All", 8, 5, 4), + EXTRA_OP("Eat/Wield/Etc.", 9, 0, 0), + OFF("Off", 0, 0, 0); + + private final String name; + private final int identifier; + private final int identifierDepositBox; + private final int identifierChambersStorageUnit; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftWithdrawMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftWithdrawMode.java new file mode 100644 index 0000000000..bbe09b1393 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/ShiftWithdrawMode.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020, Zach + * 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.menuentryswapper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.MenuAction; + +@Getter +@RequiredArgsConstructor +public enum ShiftWithdrawMode +{ + WITHDRAW_1("Withdraw-1", MenuAction.CC_OP, 2, 1), + WITHDRAW_5("Withdraw-5", MenuAction.CC_OP, 3, 2), + WITHDRAW_10("Withdraw-10", MenuAction.CC_OP, 4, 3), + WITHDRAW_X("Withdraw-X", MenuAction.CC_OP, 5, 5), + WITHDRAW_ALL("Withdraw-All", MenuAction.CC_OP_LOW_PRIORITY, 7, 4), + // chambers of xeric storage units do not have an "all-but-1" option, so this option will choose "Withdraw-all" + // instead when using the storage unit. + WITHDRAW_ALL_BUT_1("Withdraw-All-But-1", MenuAction.CC_OP_LOW_PRIORITY, 8, 4), + OFF("Off", MenuAction.UNKNOWN, 0, 0); + + private final String name; + private final MenuAction menuAction; + private final int identifier; + private final int identifierChambersStorageUnit; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/Swap.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/Swap.java new file mode 100644 index 0000000000..5cfa0a52ba --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/Swap.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020, 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.menuentryswapper; + +import java.util.function.Predicate; +import java.util.function.Supplier; +import lombok.Value; + +@Value +class Swap +{ + private Predicate optionPredicate; + private Predicate targetPredicate; + private String swappedOption; + private Supplier enabled; + private boolean strict; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePlugin.java new file mode 100644 index 0000000000..0723bb1df0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePlugin.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * Copyright (c) 2018, oplosthee + * 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.metronome; + +import com.google.inject.Provides; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Preferences; +import net.runelite.api.SoundEffectID; +import net.runelite.api.events.GameTick; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; + +@PluginDescriptor( + name = "Metronome", + description = "Play a sound on a specified tick to aid in efficient skilling", + tags = {"skilling", "tick", "timers"}, + enabledByDefault = false +) +public class MetronomePlugin extends Plugin +{ + @Inject + private Client client; + + @Inject + private MetronomePluginConfiguration config; + + private int tickCounter = 0; + private boolean shouldTock = false; + + @Provides + MetronomePluginConfiguration provideConfig(ConfigManager configManager) + { + return configManager.getConfig(MetronomePluginConfiguration.class); + } + + @Override + protected void shutDown() + { + tickCounter = 0; + shouldTock = false; + } + + @Subscribe + public void onGameTick(GameTick tick) + { + if (config.tickCount() == 0) + { + return; + } + + if (++tickCounter % config.tickCount() == 0) + { + // As playSoundEffect only uses the volume argument when the in-game volume isn't muted, sound effect volume + // needs to be set to the value desired for ticks or tocks and afterwards reset to the previous value. + Preferences preferences = client.getPreferences(); + int previousVolume = preferences.getSoundEffectVolume(); + + if (shouldTock && config.tockVolume() > 0) + { + preferences.setSoundEffectVolume(config.tockVolume()); + client.playSoundEffect(SoundEffectID.GE_DECREMENT_PLOP, config.tockVolume()); + } + else if (config.tickVolume() > 0) + { + preferences.setSoundEffectVolume(config.tickVolume()); + client.playSoundEffect(SoundEffectID.GE_INCREMENT_PLOP, config.tickVolume()); + } + + preferences.setSoundEffectVolume(previousVolume); + + shouldTock = !shouldTock; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePluginConfiguration.java b/runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePluginConfiguration.java new file mode 100644 index 0000000000..161af611ae --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/metronome/MetronomePluginConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * Copyright (c) 2018, oplosthee + * 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.metronome; + +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 net.runelite.api.SoundEffectVolume; + +@ConfigGroup("metronome") +public interface MetronomePluginConfiguration extends Config +{ + int VOLUME_MAX = SoundEffectVolume.HIGH; + + @ConfigItem( + keyName = "tickCount", + name = "Tick count", + description = "Configures the tick on which a sound will be played." + ) + default int tickCount() + { + return 1; + } + + @Range( + max = VOLUME_MAX + ) + @ConfigItem( + keyName = "tickVolume", + name = "Tick volume", + description = "Configures the volume of the tick sound. A value of 0 will disable tick sounds." + ) + default int tickVolume() + { + return SoundEffectVolume.MEDIUM_HIGH; + } + + @Range( + max = VOLUME_MAX + ) + @ConfigItem( + keyName = "tockVolume", + name = "Tock volume", + description = "Configures the volume of the tock sound. A value of 0 will disable tock sounds." + ) + default int tockVolume() + { + return SoundEffectVolume.MUTED; + } +} 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 index 8fb198ecb5..07b11ccf50 100644 --- 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 @@ -136,7 +136,7 @@ class MouseHighlightOverlay extends Overlay if (WIDGET_MENU_ACTIONS.contains(type)) { final int widgetId = menuEntry.getParam1(); - final int groupId = WidgetInfo.getGroupFromID(widgetId); + final int groupId = WidgetInfo.TO_GROUP(widgetId); if (!config.uiTooltip()) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java new file mode 100644 index 0000000000..d9e1cad9a4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java @@ -0,0 +1,79 @@ +/* + * 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.npchighlight; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.coords.WorldPoint; + +class MemorizedNpc +{ + @Getter + private int npcIndex; + + @Getter + private String npcName; + + @Getter + private int npcSize; + + /** + * The time the npc died at, in game ticks, relative to the tick counter + */ + @Getter + @Setter + private int diedOnTick; + + /** + * The time it takes for the npc to respawn, in game ticks + */ + @Getter + @Setter + private int respawnTime; + + @Getter + @Setter + private List possibleRespawnLocations; + + MemorizedNpc(NPC npc) + { + this.npcName = npc.getName(); + this.npcIndex = npc.getIndex(); + this.possibleRespawnLocations = new ArrayList<>(); + this.respawnTime = -1; + this.diedOnTick = -1; + + final NPCComposition composition = npc.getTransformedComposition(); + + if (composition != null) + { + this.npcSize = composition.getSize(); + } + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java new file mode 100644 index 0000000000..375d919aa5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.npchighlight; + +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.ConfigSection; + +@ConfigGroup("npcindicators") +public interface NpcIndicatorsConfig extends Config +{ + @ConfigSection( + name = "Render style", + description = "The render style of NPC highlighting", + position = 0 + ) + String renderStyleSection = "renderStyleSection"; + + @ConfigItem( + position = 0, + keyName = "highlightHull", + name = "Highlight hull", + description = "Configures whether or not NPC should be highlighted by hull", + section = renderStyleSection + ) + default boolean highlightHull() + { + return true; + } + + @ConfigItem( + position = 1, + keyName = "highlightTile", + name = "Highlight tile", + description = "Configures whether or not NPC should be highlighted by tile", + section = renderStyleSection + ) + default boolean highlightTile() + { + return false; + } + + @ConfigItem( + position = 2, + keyName = "highlightSouthWestTile", + name = "Highlight south west tile", + description = "Configures whether or not NPC should be highlighted by south western tile", + section = renderStyleSection + ) + default boolean highlightSouthWestTile() + { + return false; + } + + @ConfigItem( + position = 3, + keyName = "npcToHighlight", + name = "NPCs to Highlight", + description = "List of NPC names to highlight" + ) + default String getNpcToHighlight() + { + return ""; + } + + @ConfigItem( + keyName = "npcToHighlight", + name = "", + description = "" + ) + void setNpcToHighlight(String npcsToHighlight); + + @ConfigItem( + position = 4, + keyName = "npcColor", + name = "Highlight Color", + description = "Color of the NPC highlight" + ) + @Alpha + default Color getHighlightColor() + { + return Color.CYAN; + } + + @ConfigItem( + position = 5, + keyName = "drawNames", + name = "Draw names above NPC", + description = "Configures whether or not NPC names should be drawn above the NPC" + ) + default boolean drawNames() + { + return false; + } + + @ConfigItem( + position = 6, + keyName = "drawMinimapNames", + name = "Draw names on minimap", + description = "Configures whether or not NPC names should be drawn on the minimap" + ) + default boolean drawMinimapNames() + { + return false; + } + + @ConfigItem( + position = 7, + keyName = "highlightMenuNames", + name = "Highlight menu names", + description = "Highlight NPC names in right click menu" + ) + default boolean highlightMenuNames() + { + return false; + } + + @ConfigItem( + position = 8, + keyName = "ignoreDeadNpcs", + name = "Ignore dead NPCs", + description = "Prevents highlighting NPCs after they are dead" + ) + default boolean ignoreDeadNpcs() + { + return true; + } + + @ConfigItem( + position = 9, + keyName = "deadNpcMenuColor", + name = "Dead NPC menu color", + description = "Color of the NPC menus for dead NPCs" + ) + Color deadNpcMenuColor(); + + @ConfigItem( + position = 10, + keyName = "showRespawnTimer", + name = "Show respawn timer", + description = "Show respawn timer of tagged NPCs") + default boolean showRespawnTimer() + { + return false; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java new file mode 100644 index 0000000000..d8fa6e81de --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java @@ -0,0 +1,662 @@ +/* + * Copyright (c) 2018, James Swindle + * 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.npchighlight; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Provides; +import java.awt.Color; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.GraphicID; +import net.runelite.api.GraphicsObject; +import net.runelite.api.KeyCode; +import net.runelite.api.MenuAction; +import static net.runelite.api.MenuAction.MENU_ACTION_DEPRIORITIZE_OFFSET; +import net.runelite.api.MenuEntry; +import net.runelite.api.NPC; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.GraphicsObjectCreated; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +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; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.Text; +import net.runelite.client.util.WildcardMatcher; + +@PluginDescriptor( + name = "NPC Indicators", + description = "Highlight NPCs on-screen and/or on the minimap", + tags = {"highlight", "minimap", "npcs", "overlay", "respawn", "tags"} +) +@Slf4j +public class NpcIndicatorsPlugin extends Plugin +{ + private static final int MAX_ACTOR_VIEW_RANGE = 15; + + // Option added to NPC menu + private static final String TAG = "Tag"; + private static final String UNTAG = "Un-tag"; + + private static final String TAG_ALL = "Tag-All"; + private static final String UNTAG_ALL = "Un-tag-All"; + + private static final Set NPC_MENU_ACTIONS = ImmutableSet.of(MenuAction.NPC_FIRST_OPTION, MenuAction.NPC_SECOND_OPTION, + MenuAction.NPC_THIRD_OPTION, MenuAction.NPC_FOURTH_OPTION, MenuAction.NPC_FIFTH_OPTION, MenuAction.SPELL_CAST_ON_NPC, + MenuAction.ITEM_USE_ON_NPC); + + @Inject + private Client client; + + @Inject + private NpcIndicatorsConfig config; + + @Inject + private OverlayManager overlayManager; + + @Inject + private NpcSceneOverlay npcSceneOverlay; + + @Inject + private NpcMinimapOverlay npcMinimapOverlay; + + @Inject + private ClientThread clientThread; + + /** + * NPCs to highlight + */ + @Getter(AccessLevel.PACKAGE) + private final Set highlightedNpcs = new HashSet<>(); + + /** + * Dead NPCs that should be displayed with a respawn indicator if the config is on. + */ + @Getter(AccessLevel.PACKAGE) + private final Map deadNpcsToDisplay = new HashMap<>(); + + /** + * The time when the last game tick event ran. + */ + @Getter(AccessLevel.PACKAGE) + private Instant lastTickUpdate; + + /** + * Tagged NPCs that have died at some point, which are memorized to + * remember when and where they will respawn + */ + private final Map memorizedNpcs = new HashMap<>(); + + /** + * Highlight strings from the configuration + */ + private List highlights = new ArrayList<>(); + + /** + * NPC ids marked with the Tag option + */ + private final Set npcTags = new HashSet<>(); + + /** + * Tagged NPCs that spawned this tick, which need to be verified that + * they actually spawned and didn't just walk into view range. + */ + private final List spawnedNpcsThisTick = new ArrayList<>(); + + /** + * Tagged NPCs that despawned this tick, which need to be verified that + * they actually spawned and didn't just walk into view range. + */ + private final List despawnedNpcsThisTick = new ArrayList<>(); + + /** + * World locations of graphics object which indicate that an + * NPC teleported that were played this tick. + */ + private final Set teleportGraphicsObjectSpawnedThisTick = new HashSet<>(); + + /** + * The players location on the last game tick. + */ + private WorldPoint lastPlayerLocation; + + /** + * When hopping worlds, NPCs can spawn without them actually respawning, + * so we would not want to mark it as a real spawn in those cases. + */ + private boolean skipNextSpawnCheck = false; + + @Provides + NpcIndicatorsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(NpcIndicatorsConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(npcSceneOverlay); + overlayManager.add(npcMinimapOverlay); + clientThread.invoke(() -> + { + skipNextSpawnCheck = true; + rebuildAllNpcs(); + }); + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(npcSceneOverlay); + overlayManager.remove(npcMinimapOverlay); + clientThread.invoke(() -> + { + deadNpcsToDisplay.clear(); + memorizedNpcs.clear(); + spawnedNpcsThisTick.clear(); + despawnedNpcsThisTick.clear(); + teleportGraphicsObjectSpawnedThisTick.clear(); + npcTags.clear(); + highlightedNpcs.clear(); + }); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOGIN_SCREEN || + event.getGameState() == GameState.HOPPING) + { + highlightedNpcs.clear(); + deadNpcsToDisplay.clear(); + memorizedNpcs.forEach((id, npc) -> npc.setDiedOnTick(-1)); + lastPlayerLocation = null; + skipNextSpawnCheck = true; + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged configChanged) + { + if (!configChanged.getGroup().equals("npcindicators")) + { + return; + } + + clientThread.invoke(this::rebuildAllNpcs); + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded event) + { + int type = event.getType(); + + if (type >= MENU_ACTION_DEPRIORITIZE_OFFSET) + { + type -= MENU_ACTION_DEPRIORITIZE_OFFSET; + } + + final MenuAction menuAction = MenuAction.of(type); + + if (NPC_MENU_ACTIONS.contains(menuAction)) + { + NPC npc = client.getCachedNPCs()[event.getIdentifier()]; + + Color color = null; + if (npc.isDead()) + { + color = config.deadNpcMenuColor(); + } + + if (color == null && highlightedNpcs.contains(npc) && config.highlightMenuNames() && (!npc.isDead() || !config.ignoreDeadNpcs())) + { + color = config.getHighlightColor(); + } + + if (color != null) + { + MenuEntry[] menuEntries = client.getMenuEntries(); + final MenuEntry menuEntry = menuEntries[menuEntries.length - 1]; + final String target = ColorUtil.prependColorTag(Text.removeTags(event.getTarget()), color); + menuEntry.setTarget(target); + client.setMenuEntries(menuEntries); + } + } + else if (menuAction == MenuAction.EXAMINE_NPC && client.isKeyPressed(KeyCode.KC_SHIFT)) + { + // Add tag and tag-all options + final int id = event.getIdentifier(); + final NPC[] cachedNPCs = client.getCachedNPCs(); + final NPC npc = cachedNPCs[id]; + + if (npc == null || npc.getName() == null) + { + return; + } + + final String npcName = npc.getName(); + boolean matchesList = highlights.stream() + .filter(highlight -> !highlight.equalsIgnoreCase(npcName)) + .anyMatch(highlight -> WildcardMatcher.matches(highlight, npcName)); + + MenuEntry[] menuEntries = client.getMenuEntries(); + + // Only add Untag-All option to npcs not highlighted by a wildcard entry, because untag-all will not remove wildcards + if (!matchesList) + { + menuEntries = Arrays.copyOf(menuEntries, menuEntries.length + 2); + final MenuEntry tagAllEntry = menuEntries[menuEntries.length - 2] = new MenuEntry(); + tagAllEntry.setOption(highlights.stream().anyMatch(npcName::equalsIgnoreCase) ? UNTAG_ALL : TAG_ALL); + tagAllEntry.setTarget(event.getTarget()); + tagAllEntry.setParam0(event.getActionParam0()); + tagAllEntry.setParam1(event.getActionParam1()); + tagAllEntry.setIdentifier(event.getIdentifier()); + tagAllEntry.setType(MenuAction.RUNELITE.getId()); + } + else + { + menuEntries = Arrays.copyOf(menuEntries, menuEntries.length + 1); + } + + final MenuEntry tagEntry = menuEntries[menuEntries.length - 1] = new MenuEntry(); + tagEntry.setOption(npcTags.contains(npc.getIndex()) ? UNTAG : TAG); + tagEntry.setTarget(event.getTarget()); + tagEntry.setParam0(event.getActionParam0()); + tagEntry.setParam1(event.getActionParam1()); + tagEntry.setIdentifier(event.getIdentifier()); + tagEntry.setType(MenuAction.RUNELITE.getId()); + + client.setMenuEntries(menuEntries); + } + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked click) + { + if (click.getMenuAction() != MenuAction.RUNELITE || + !(click.getMenuOption().equals(TAG) || click.getMenuOption().equals(UNTAG) || + click.getMenuOption().equals(TAG_ALL) || click.getMenuOption().equals(UNTAG_ALL))) + { + return; + } + + final int id = click.getId(); + final NPC[] cachedNPCs = client.getCachedNPCs(); + final NPC npc = cachedNPCs[id]; + + if (npc == null || npc.getName() == null) + { + return; + } + + if (click.getMenuOption().equals(TAG) || click.getMenuOption().equals(UNTAG)) + { + final boolean removed = npcTags.remove(id); + + if (removed) + { + if (!highlightMatchesNPCName(npc.getName())) + { + highlightedNpcs.remove(npc); + memorizedNpcs.remove(npc.getIndex()); + } + } + else + { + if (!client.isInInstancedRegion()) + { + memorizeNpc(npc); + npcTags.add(id); + } + highlightedNpcs.add(npc); + } + } + else + { + final String name = npc.getName(); + updateNpcsToHighlight(name); + } + + click.consume(); + } + + @Subscribe + public void onNpcSpawned(NpcSpawned npcSpawned) + { + final NPC npc = npcSpawned.getNpc(); + final String npcName = npc.getName(); + + if (npcName == null) + { + return; + } + + if (npcTags.contains(npc.getIndex())) + { + memorizeNpc(npc); + highlightedNpcs.add(npc); + spawnedNpcsThisTick.add(npc); + return; + } + + if (highlightMatchesNPCName(npcName)) + { + highlightedNpcs.add(npc); + if (!client.isInInstancedRegion()) + { + memorizeNpc(npc); + spawnedNpcsThisTick.add(npc); + } + } + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + final NPC npc = npcDespawned.getNpc(); + + if (memorizedNpcs.containsKey(npc.getIndex())) + { + despawnedNpcsThisTick.add(npc); + } + + highlightedNpcs.remove(npc); + } + + @Subscribe + public void onGraphicsObjectCreated(GraphicsObjectCreated event) + { + final GraphicsObject go = event.getGraphicsObject(); + + if (go.getId() == GraphicID.GREY_BUBBLE_TELEPORT) + { + teleportGraphicsObjectSpawnedThisTick.add(WorldPoint.fromLocal(client, go.getLocation())); + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + removeOldHighlightedRespawns(); + validateSpawnedNpcs(); + lastTickUpdate = Instant.now(); + lastPlayerLocation = client.getLocalPlayer().getWorldLocation(); + } + + private void updateNpcsToHighlight(String npc) + { + final List highlightedNpcs = new ArrayList<>(highlights); + + if (!highlightedNpcs.removeIf(npc::equalsIgnoreCase)) + { + highlightedNpcs.add(npc); + } + + // this triggers the config change event and rebuilds npcs + config.setNpcToHighlight(Text.toCSV(highlightedNpcs)); + } + + private static boolean isInViewRange(WorldPoint wp1, WorldPoint wp2) + { + int distance = wp1.distanceTo(wp2); + return distance < MAX_ACTOR_VIEW_RANGE; + } + + private static WorldPoint getWorldLocationBehind(NPC npc) + { + final int orientation = npc.getOrientation() / 256; + int dx = 0, dy = 0; + + switch (orientation) + { + case 0: // South + dy = -1; + break; + case 1: // Southwest + dx = -1; + dy = -1; + break; + case 2: // West + dx = -1; + break; + case 3: // Northwest + dx = -1; + dy = 1; + break; + case 4: // North + dy = 1; + break; + case 5: // Northeast + dx = 1; + dy = 1; + break; + case 6: // East + dx = 1; + break; + case 7: // Southeast + dx = 1; + dy = -1; + break; + } + + final WorldPoint currWP = npc.getWorldLocation(); + return new WorldPoint(currWP.getX() - dx, currWP.getY() - dy, currWP.getPlane()); + } + + private void memorizeNpc(NPC npc) + { + final int npcIndex = npc.getIndex(); + memorizedNpcs.putIfAbsent(npcIndex, new MemorizedNpc(npc)); + } + + private void removeOldHighlightedRespawns() + { + deadNpcsToDisplay.values().removeIf(x -> x.getDiedOnTick() + x.getRespawnTime() <= client.getTickCount() + 1); + } + + @VisibleForTesting + List getHighlights() + { + final String configNpcs = config.getNpcToHighlight(); + + if (configNpcs.isEmpty()) + { + return Collections.emptyList(); + } + + return Text.fromCSV(configNpcs); + } + + @VisibleForTesting + void rebuildAllNpcs() + { + highlights = getHighlights(); + highlightedNpcs.clear(); + + if (client.getGameState() != GameState.LOGGED_IN && + client.getGameState() != GameState.LOADING) + { + // NPCs are still in the client after logging out, + // but we don't want to highlight those. + return; + } + + for (NPC npc : client.getNpcs()) + { + final String npcName = npc.getName(); + + if (npcName == null) + { + continue; + } + + if (npcTags.contains(npc.getIndex())) + { + highlightedNpcs.add(npc); + continue; + } + + if (highlightMatchesNPCName(npcName)) + { + if (!client.isInInstancedRegion()) + { + memorizeNpc(npc); + } + highlightedNpcs.add(npc); + continue; + } + + // NPC is not highlighted + memorizedNpcs.remove(npc.getIndex()); + } + } + + private boolean highlightMatchesNPCName(String npcName) + { + for (String highlight : highlights) + { + if (WildcardMatcher.matches(highlight, npcName)) + { + return true; + } + } + + return false; + } + + private void validateSpawnedNpcs() + { + if (skipNextSpawnCheck) + { + skipNextSpawnCheck = false; + } + else + { + for (NPC npc : despawnedNpcsThisTick) + { + if (!teleportGraphicsObjectSpawnedThisTick.isEmpty()) + { + if (teleportGraphicsObjectSpawnedThisTick.contains(npc.getWorldLocation())) + { + // NPC teleported away, so we don't want to add the respawn timer + continue; + } + } + + if (isInViewRange(client.getLocalPlayer().getWorldLocation(), npc.getWorldLocation())) + { + final MemorizedNpc mn = memorizedNpcs.get(npc.getIndex()); + + if (mn != null) + { + mn.setDiedOnTick(client.getTickCount() + 1); // This runs before tickCounter updates, so we add 1 + + if (!mn.getPossibleRespawnLocations().isEmpty()) + { + log.debug("Starting {} tick countdown for {}", mn.getRespawnTime(), mn.getNpcName()); + deadNpcsToDisplay.put(mn.getNpcIndex(), mn); + } + } + } + } + + for (NPC npc : spawnedNpcsThisTick) + { + if (!teleportGraphicsObjectSpawnedThisTick.isEmpty()) + { + if (teleportGraphicsObjectSpawnedThisTick.contains(npc.getWorldLocation()) || + teleportGraphicsObjectSpawnedThisTick.contains(getWorldLocationBehind(npc))) + { + // NPC teleported here, so we don't want to update the respawn timer + continue; + } + } + + if (lastPlayerLocation != null && isInViewRange(lastPlayerLocation, npc.getWorldLocation())) + { + final MemorizedNpc mn = memorizedNpcs.get(npc.getIndex()); + + if (mn.getDiedOnTick() != -1) + { + final int respawnTime = client.getTickCount() + 1 - mn.getDiedOnTick(); + + // By killing a monster and leaving the area before seeing it again, an erroneously lengthy + // respawn time can be recorded. Thus, if the respawn time is already set and is greater than + // the observed time, assume that the lower observed respawn time is correct. + if (mn.getRespawnTime() == -1 || respawnTime < mn.getRespawnTime()) + { + mn.setRespawnTime(respawnTime); + } + + mn.setDiedOnTick(-1); + } + + final WorldPoint npcLocation = npc.getWorldLocation(); + + // An NPC can move in the same tick as it spawns, so we also have + // to consider whatever tile is behind the npc + final WorldPoint possibleOtherNpcLocation = getWorldLocationBehind(npc); + + mn.getPossibleRespawnLocations().removeIf(x -> + x.distanceTo(npcLocation) != 0 && x.distanceTo(possibleOtherNpcLocation) != 0); + + if (mn.getPossibleRespawnLocations().isEmpty()) + { + mn.getPossibleRespawnLocations().add(npcLocation); + mn.getPossibleRespawnLocations().add(possibleOtherNpcLocation); + } + } + } + } + + spawnedNpcsThisTick.clear(); + despawnedNpcsThisTick.clear(); + teleportGraphicsObjectSpawnedThisTick.clear(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcMinimapOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcMinimapOverlay.java new file mode 100644 index 0000000000..9582288ea1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcMinimapOverlay.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2018, James Swindle + * 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.npchighlight; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Point; +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.OverlayUtil; + +public class NpcMinimapOverlay extends Overlay +{ + private final NpcIndicatorsConfig config; + private final NpcIndicatorsPlugin plugin; + + @Inject + NpcMinimapOverlay(NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin) + { + this.config = config; + this.plugin = plugin; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_WIDGETS); + } + + @Override + public Dimension render(Graphics2D graphics) + { + for (NPC npc : plugin.getHighlightedNpcs()) + { + renderNpcOverlay(graphics, npc, npc.getName(), config.getHighlightColor()); + } + + return null; + } + + private void renderNpcOverlay(Graphics2D graphics, NPC actor, String name, Color color) + { + NPCComposition npcComposition = actor.getTransformedComposition(); + if (npcComposition == null || !npcComposition.isInteractible() + || (actor.isDead() && config.ignoreDeadNpcs())) + { + return; + } + + Point minimapLocation = actor.getMinimapLocation(); + if (minimapLocation != null) + { + OverlayUtil.renderMinimapLocation(graphics, minimapLocation, color.darker()); + + if (config.drawMinimapNames()) + { + OverlayUtil.renderTextLocation(graphics, minimapLocation, name, color); + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java new file mode 100644 index 0000000000..441d811a97 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2018, James Swindle + * 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.npchighlight; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Shape; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.time.Instant; +import java.util.Locale; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Constants; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +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.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; +import net.runelite.client.util.Text; + +public class NpcSceneOverlay extends Overlay +{ + // Anything but white text is quite hard to see since it is drawn on + // a dark background + private static final Color TEXT_COLOR = Color.WHITE; + + private static final NumberFormat TIME_LEFT_FORMATTER = DecimalFormat.getInstance(Locale.US); + + static + { + ((DecimalFormat)TIME_LEFT_FORMATTER).applyPattern("#0.0"); + } + + private final Client client; + private final NpcIndicatorsConfig config; + private final NpcIndicatorsPlugin plugin; + + @Inject + NpcSceneOverlay(Client client, NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin) + { + this.client = client; + this.config = config; + this.plugin = plugin; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (config.showRespawnTimer()) + { + plugin.getDeadNpcsToDisplay().forEach((id, npc) -> renderNpcRespawn(npc, graphics)); + } + + for (NPC npc : plugin.getHighlightedNpcs()) + { + renderNpcOverlay(graphics, npc, config.getHighlightColor()); + } + + return null; + } + + private void renderNpcRespawn(final MemorizedNpc npc, final Graphics2D graphics) + { + if (npc.getPossibleRespawnLocations().isEmpty()) + { + return; + } + + final WorldPoint respawnLocation = npc.getPossibleRespawnLocations().get(0); + final LocalPoint lp = LocalPoint.fromWorld(client, respawnLocation.getX(), respawnLocation.getY()); + + if (lp == null) + { + return; + } + + final Color color = config.getHighlightColor(); + + final LocalPoint centerLp = new LocalPoint( + lp.getX() + Perspective.LOCAL_TILE_SIZE * (npc.getNpcSize() - 1) / 2, + lp.getY() + Perspective.LOCAL_TILE_SIZE * (npc.getNpcSize() - 1) / 2); + + final Polygon poly = Perspective.getCanvasTileAreaPoly(client, centerLp, npc.getNpcSize()); + + if (poly != null) + { + OverlayUtil.renderPolygon(graphics, poly, color); + } + + final Instant now = Instant.now(); + final double baseTick = ((npc.getDiedOnTick() + npc.getRespawnTime()) - client.getTickCount()) * (Constants.GAME_TICK_LENGTH / 1000.0); + final double sinceLast = (now.toEpochMilli() - plugin.getLastTickUpdate().toEpochMilli()) / 1000.0; + final double timeLeft = Math.max(0.0, baseTick - sinceLast); + final String timeLeftStr = TIME_LEFT_FORMATTER.format(timeLeft); + + final int textWidth = graphics.getFontMetrics().stringWidth(timeLeftStr); + final int textHeight = graphics.getFontMetrics().getAscent(); + + final Point canvasPoint = Perspective + .localToCanvas(client, centerLp, respawnLocation.getPlane()); + + if (canvasPoint != null) + { + final Point canvasCenterPoint = new Point( + canvasPoint.getX() - textWidth / 2, + canvasPoint.getY() + textHeight / 2); + + OverlayUtil.renderTextLocation(graphics, canvasCenterPoint, timeLeftStr, TEXT_COLOR); + } + } + + private void renderNpcOverlay(Graphics2D graphics, NPC actor, Color color) + { + NPCComposition npcComposition = actor.getTransformedComposition(); + if (npcComposition == null || !npcComposition.isInteractible() + || (actor.isDead() && config.ignoreDeadNpcs())) + { + return; + } + + if (config.highlightHull()) + { + Shape objectClickbox = actor.getConvexHull(); + renderPoly(graphics, color, objectClickbox); + } + + if (config.highlightTile()) + { + int size = npcComposition.getSize(); + LocalPoint lp = actor.getLocalLocation(); + Polygon tilePoly = Perspective.getCanvasTileAreaPoly(client, lp, size); + + renderPoly(graphics, color, tilePoly); + } + + if (config.highlightSouthWestTile()) + { + int size = npcComposition.getSize(); + LocalPoint lp = actor.getLocalLocation(); + + int x = lp.getX() - ((size - 1) * Perspective.LOCAL_TILE_SIZE / 2); + int y = lp.getY() - ((size - 1) * Perspective.LOCAL_TILE_SIZE / 2); + + Polygon southWestTilePoly = Perspective.getCanvasTilePoly(client, new LocalPoint(x, y)); + + renderPoly(graphics, color, southWestTilePoly); + } + + if (config.drawNames() && actor.getName() != null) + { + String npcName = Text.removeTags(actor.getName()); + Point textLocation = actor.getCanvasTextLocation(graphics, npcName, actor.getLogicalHeight() + 40); + + if (textLocation != null) + { + OverlayUtil.renderTextLocation(graphics, textLocation, npcName, color); + } + } + } + + private void renderPoly(Graphics2D graphics, Color color, Shape polygon) + { + if (polygon != null) + { + graphics.setColor(color); + graphics.setStroke(new BasicStroke(2)); + graphics.draw(polygon); + graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20)); + graphics.fill(polygon); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java new file mode 100644 index 0000000000..7194d2ddd6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java @@ -0,0 +1,68 @@ +/* + * 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.npcunaggroarea; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import lombok.Getter; +import lombok.Setter; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.infobox.Timer; + +class AggressionTimer extends Timer +{ + @Getter + @Setter + private boolean visible; + + AggressionTimer(Duration duration, BufferedImage image, Plugin plugin, boolean visible) + { + super(duration.toMillis(), ChronoUnit.MILLIS, image, plugin); + setTooltip("Time until NPCs become unaggressive"); + this.visible = visible; + } + + @Override + public Color getTextColor() + { + Duration timeLeft = Duration.between(Instant.now(), getEndTime()); + + if (timeLeft.getSeconds() < 60) + { + return Color.RED.brighter(); + } + + return Color.WHITE; + } + + @Override + public boolean render() + { + return visible && super.render(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java new file mode 100644 index 0000000000..223695fa64 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java @@ -0,0 +1,120 @@ +/* + * 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.npcunaggroarea; + +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; + +@ConfigGroup("npcUnaggroArea") +public interface NpcAggroAreaConfig extends Config +{ + String CONFIG_GROUP = "npcUnaggroArea"; + String CONFIG_CENTER1 = "center1"; + String CONFIG_CENTER2 = "center2"; + String CONFIG_LOCATION = "location"; + String CONFIG_DURATION = "duration"; + + @ConfigItem( + keyName = "npcUnaggroAlwaysActive", + name = "Always active", + description = "Always show this plugins overlays
Otherwise, they will only be shown when any NPC name matches the list", + position = 1 + ) + default boolean alwaysActive() + { + return false; + } + + @ConfigItem( + keyName = "npcUnaggroNames", + name = "NPC names", + description = "Enter names of NPCs where you wish to use this plugin", + position = 2 + ) + default String npcNamePatterns() + { + return ""; + } + + @ConfigItem( + keyName = "npcUnaggroShowTimer", + name = "Show timer", + description = "Display a timer until NPCs become unaggressive", + position = 3 + ) + default boolean showTimer() + { + return true; + } + + @ConfigItem( + keyName = "npcUnaggroShowAreaLines", + name = "Show area lines", + description = "Display lines, when walked past, the unaggressive timer resets", + position = 4 + ) + default boolean showAreaLines() + { + return false; + } + + @ConfigItem( + keyName = "npcAggroAreaColor", + name = "Aggressive colour", + description = "Choose colour to use for marking NPC unaggressive area when NPCs are aggressive", + position = 5 + ) + @Alpha + default Color aggroAreaColor() + { + return new Color(0x64FFFF00, true); + } + + @ConfigItem( + keyName = "npcUnaggroAreaColor", + name = "Unaggressive colour", + description = "Choose colour to use for marking NPC unaggressive area after NPCs have lost aggression", + position = 6 + ) + @Alpha + default Color unaggroAreaColor() + { + return new Color(0xFFFF00); + } + + @ConfigItem( + keyName = "notifyExpire", + name = "Notify Expiration", + description = "Send a notifcation when the unaggressive timer expires", + position = 7 + ) + default boolean notifyExpire() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java new file mode 100644 index 0000000000..33e058f686 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java @@ -0,0 +1,63 @@ +/* + * 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.npcunaggroarea; + +import com.google.inject.Inject; +import java.awt.Dimension; +import java.awt.Graphics2D; +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; + +class NpcAggroAreaNotWorkingOverlay extends OverlayPanel +{ + private final NpcAggroAreaPlugin plugin; + + @Inject + private NpcAggroAreaNotWorkingOverlay(NpcAggroAreaPlugin plugin) + { + this.plugin = plugin; + + panelComponent.getChildren().add(LineComponent.builder() + .left("Unaggressive NPC timers will start working when you teleport far away or enter a dungeon.") + .build()); + + setPriority(OverlayPriority.LOW); + setPosition(OverlayPosition.TOP_LEFT); + setClearChildren(false); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!plugin.isActive() || plugin.getSafeCenters()[1] != null) + { + return null; + } + + return super.render(graphics); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java new file mode 100644 index 0000000000..2e77daa25e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java @@ -0,0 +1,115 @@ +/* + * 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.npcunaggroarea; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.GeneralPath; +import java.time.Instant; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.geometry.Geometry; +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.OverlayPriority; + +class NpcAggroAreaOverlay extends Overlay +{ + private static final int MAX_LOCAL_DRAW_LENGTH = 20 * Perspective.LOCAL_TILE_SIZE; + + private final Client client; + private final NpcAggroAreaConfig config; + private final NpcAggroAreaPlugin plugin; + + @Inject + private NpcAggroAreaOverlay(Client client, NpcAggroAreaConfig config, NpcAggroAreaPlugin plugin) + { + this.client = client; + this.config = config; + this.plugin = plugin; + + setLayer(OverlayLayer.ABOVE_SCENE); + setPriority(OverlayPriority.LOW); + setPosition(OverlayPosition.DYNAMIC); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!plugin.isActive() || plugin.getSafeCenters()[1] == null) + { + return null; + } + + GeneralPath lines = plugin.getLinesToDisplay()[client.getPlane()]; + if (lines == null) + { + return null; + } + + Color outlineColor = config.unaggroAreaColor(); + AggressionTimer timer = plugin.getCurrentTimer(); + if (outlineColor == null || timer == null || Instant.now().compareTo(timer.getEndTime()) < 0) + { + outlineColor = config.aggroAreaColor(); + } + + renderPath(graphics, lines, outlineColor); + return null; + } + + private void renderPath(Graphics2D graphics, GeneralPath path, Color color) + { + LocalPoint playerLp = client.getLocalPlayer().getLocalLocation(); + Rectangle viewArea = new Rectangle( + playerLp.getX() - MAX_LOCAL_DRAW_LENGTH, + playerLp.getY() - MAX_LOCAL_DRAW_LENGTH, + MAX_LOCAL_DRAW_LENGTH * 2, + MAX_LOCAL_DRAW_LENGTH * 2); + + graphics.setColor(color); + graphics.setStroke(new BasicStroke(1)); + + path = Geometry.clipPath(path, viewArea); + path = Geometry.filterPath(path, (p1, p2) -> + Perspective.localToCanvas(client, new LocalPoint((int)p1[0], (int)p1[1]), client.getPlane()) != null && + Perspective.localToCanvas(client, new LocalPoint((int)p2[0], (int)p2[1]), client.getPlane()) != null); + path = Geometry.transformPath(path, coords -> + { + Point point = Perspective.localToCanvas(client, new LocalPoint((int)coords[0], (int)coords[1]), client.getPlane()); + coords[0] = point.getX(); + coords[1] = point.getY(); + }); + + graphics.draw(path); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java new file mode 100644 index 0000000000..cf4830ebd7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java @@ -0,0 +1,492 @@ +/* + * 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.npcunaggroarea; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.inject.Provides; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.geom.Area; +import java.awt.geom.GeneralPath; +import java.awt.image.BufferedImage; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import javax.inject.Inject; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.Constants; +import net.runelite.api.ItemID; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Perspective; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.events.ConfigChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.geometry.Geometry; +import net.runelite.client.Notifier; +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.WildcardMatcher; + +@PluginDescriptor( + name = "NPC Aggression Timer", + description = "Highlights the unaggressive area of NPCs nearby and timer until it becomes active", + tags = {"highlight", "lines", "unaggro", "aggro", "aggressive", "npcs", "area", "slayer"}, + enabledByDefault = false +) +public class NpcAggroAreaPlugin extends Plugin +{ + /* + How it works: The game remembers 2 tiles. When the player goes >10 steps + away from both tiles, the oldest one is moved to under the player and the + NPC aggression timer resets. + So to first figure out where the 2 tiles are, we wait until the player teleports + a long enough distance. At that point it's very likely that the player + moved out of the radius of both tiles, which resets one of them. The other + should reset shortly after as the player starts moving around. + */ + + private static final int SAFE_AREA_RADIUS = 10; + private static final int UNKNOWN_AREA_RADIUS = SAFE_AREA_RADIUS * 2; + private static final int AGGRESSIVE_TIME_SECONDS = 600; + private static final Splitter NAME_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults(); + private static final WorldArea WILDERNESS_ABOVE_GROUND = new WorldArea(2944, 3523, 448, 448, 0); + private static final WorldArea WILDERNESS_UNDERGROUND = new WorldArea(2944, 9918, 320, 442, 0); + + @Inject + private Client client; + + @Inject + private NpcAggroAreaConfig config; + + @Inject + private NpcAggroAreaOverlay overlay; + + @Inject + private NpcAggroAreaNotWorkingOverlay notWorkingOverlay; + + @Inject + private OverlayManager overlayManager; + + @Inject + private ItemManager itemManager; + + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private ConfigManager configManager; + + @Inject + private Notifier notifier; + + @Getter + private final WorldPoint[] safeCenters = new WorldPoint[2]; + + @Getter + private final GeneralPath[] linesToDisplay = new GeneralPath[Constants.MAX_Z]; + + @Getter + private boolean active; + + @Getter + private AggressionTimer currentTimer; + + private WorldPoint lastPlayerLocation; + private WorldPoint previousUnknownCenter; + private boolean loggingIn; + private boolean notifyOnce; + + private List npcNamePatterns; + + @Provides + NpcAggroAreaConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(NpcAggroAreaConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(overlay); + overlayManager.add(notWorkingOverlay); + npcNamePatterns = NAME_SPLITTER.splitToList(config.npcNamePatterns()); + recheckActive(); + } + + @Override + protected void shutDown() throws Exception + { + removeTimer(); + overlayManager.remove(overlay); + overlayManager.remove(notWorkingOverlay); + Arrays.fill(safeCenters, null); + lastPlayerLocation = null; + currentTimer = null; + loggingIn = false; + npcNamePatterns = null; + active = false; + + Arrays.fill(linesToDisplay, null); + } + + private Area generateSafeArea() + { + final Area area = new Area(); + + for (WorldPoint wp : safeCenters) + { + if (wp == null) + { + continue; + } + + Polygon poly = new Polygon(); + poly.addPoint(wp.getX() - SAFE_AREA_RADIUS, wp.getY() - SAFE_AREA_RADIUS); + poly.addPoint(wp.getX() - SAFE_AREA_RADIUS, wp.getY() + SAFE_AREA_RADIUS + 1); + poly.addPoint(wp.getX() + SAFE_AREA_RADIUS + 1, wp.getY() + SAFE_AREA_RADIUS + 1); + poly.addPoint(wp.getX() + SAFE_AREA_RADIUS + 1, wp.getY() - SAFE_AREA_RADIUS); + area.add(new Area(poly)); + } + + return area; + } + + private void transformWorldToLocal(float[] coords) + { + final LocalPoint lp = LocalPoint.fromWorld(client, (int)coords[0], (int)coords[1]); + coords[0] = lp.getX() - Perspective.LOCAL_TILE_SIZE / 2f; + coords[1] = lp.getY() - Perspective.LOCAL_TILE_SIZE / 2f; + } + + private void reevaluateActive() + { + if (currentTimer != null) + { + currentTimer.setVisible(active && config.showTimer()); + } + + calculateLinesToDisplay(); + } + + private void calculateLinesToDisplay() + { + if (!active || !config.showAreaLines()) + { + Arrays.fill(linesToDisplay, null); + return; + } + + Rectangle sceneRect = new Rectangle( + client.getBaseX() + 1, client.getBaseY() + 1, + Constants.SCENE_SIZE - 2, Constants.SCENE_SIZE - 2); + + for (int i = 0; i < linesToDisplay.length; i++) + { + GeneralPath lines = new GeneralPath(generateSafeArea()); + lines = Geometry.clipPath(lines, sceneRect); + lines = Geometry.splitIntoSegments(lines, 1); + lines = Geometry.transformPath(lines, this::transformWorldToLocal); + linesToDisplay[i] = lines; + } + } + + private void removeTimer() + { + infoBoxManager.removeInfoBox(currentTimer); + currentTimer = null; + notifyOnce = false; + } + + private void createTimer(Duration duration) + { + removeTimer(); + BufferedImage image = itemManager.getImage(ItemID.ENSOULED_DEMON_HEAD); + currentTimer = new AggressionTimer(duration, image, this, active && config.showTimer()); + infoBoxManager.addInfoBox(currentTimer); + notifyOnce = true; + } + + private void resetTimer() + { + createTimer(Duration.ofSeconds(AGGRESSIVE_TIME_SECONDS)); + } + + private static boolean isInWilderness(WorldPoint location) + { + return WILDERNESS_ABOVE_GROUND.distanceTo2D(location) == 0 || WILDERNESS_UNDERGROUND.distanceTo2D(location) == 0; + } + + private boolean isNpcMatch(NPC npc) + { + NPCComposition composition = npc.getTransformedComposition(); + if (composition == null) + { + return false; + } + + if (Strings.isNullOrEmpty(composition.getName())) + { + return false; + } + + // Most NPCs stop aggroing when the player has more than double + // its combat level. + int playerLvl = client.getLocalPlayer().getCombatLevel(); + int npcLvl = composition.getCombatLevel(); + String npcName = composition.getName().toLowerCase(); + if (npcLvl > 0 && playerLvl > npcLvl * 2 && !isInWilderness(npc.getWorldLocation())) + { + return false; + } + + for (String pattern : npcNamePatterns) + { + if (WildcardMatcher.matches(pattern, npcName)) + { + return true; + } + } + + return false; + } + + private void checkAreaNpcs(final NPC... npcs) + { + for (NPC npc : npcs) + { + if (npc == null) + { + continue; + } + + if (isNpcMatch(npc)) + { + active = true; + break; + } + } + + reevaluateActive(); + } + + private void recheckActive() + { + active = config.alwaysActive(); + checkAreaNpcs(client.getCachedNPCs()); + } + + @Subscribe + public void onNpcSpawned(NpcSpawned event) + { + if (config.alwaysActive()) + { + return; + } + + checkAreaNpcs(event.getNpc()); + } + + @Subscribe + public void onGameTick(GameTick event) + { + WorldPoint newLocation = client.getLocalPlayer().getWorldLocation(); + + if (active && currentTimer != null && currentTimer.cull() && notifyOnce) + { + if (config.notifyExpire()) + { + notifier.notify("NPC aggression has expired!"); + } + + notifyOnce = false; + } + + if (lastPlayerLocation != null) + { + if (safeCenters[1] == null && newLocation.distanceTo2D(lastPlayerLocation) > SAFE_AREA_RADIUS * 4) + { + safeCenters[0] = null; + safeCenters[1] = newLocation; + resetTimer(); + calculateLinesToDisplay(); + + // We don't know where the previous area was, so if the player e.g. + // entered a dungeon and then goes back out, he/she may enter the previous + // area which is unknown and would make the plugin inaccurate + previousUnknownCenter = lastPlayerLocation; + } + } + + if (safeCenters[0] == null && previousUnknownCenter != null && + previousUnknownCenter.distanceTo2D(newLocation) <= UNKNOWN_AREA_RADIUS) + { + // Player went back to their previous unknown area before the 2nd + // center point was found, which means we don't know where it is again. + safeCenters[1] = null; + removeTimer(); + calculateLinesToDisplay(); + } + + if (safeCenters[1] != null) + { + if (Arrays.stream(safeCenters).noneMatch( + x -> x != null && x.distanceTo2D(newLocation) <= SAFE_AREA_RADIUS)) + { + safeCenters[0] = safeCenters[1]; + safeCenters[1] = newLocation; + resetTimer(); + calculateLinesToDisplay(); + previousUnknownCenter = null; + } + } + + lastPlayerLocation = newLocation; + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + String key = event.getKey(); + switch (key) + { + case "npcUnaggroAlwaysActive": + recheckActive(); + break; + case "npcUnaggroShowTimer": + if (currentTimer != null) + { + currentTimer.setVisible(active && config.showTimer()); + } + break; + case "npcUnaggroCollisionDetection": + case "npcUnaggroShowAreaLines": + calculateLinesToDisplay(); + break; + case "npcUnaggroNames": + npcNamePatterns = NAME_SPLITTER.splitToList(config.npcNamePatterns()); + recheckActive(); + break; + } + } + + private void loadConfig() + { + safeCenters[0] = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1, WorldPoint.class); + safeCenters[1] = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2, WorldPoint.class); + lastPlayerLocation = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION, WorldPoint.class); + + Duration timeLeft = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION, Duration.class); + if (timeLeft != null && !timeLeft.isNegative()) + { + createTimer(timeLeft); + } + } + + private void resetConfig() + { + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1); + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2); + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION); + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION); + } + + private void saveConfig() + { + if (safeCenters[0] == null || safeCenters[1] == null || lastPlayerLocation == null || currentTimer == null) + { + resetConfig(); + } + else + { + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1, safeCenters[0]); + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2, safeCenters[1]); + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION, lastPlayerLocation); + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION, Duration.between(Instant.now(), currentTimer.getEndTime())); + } + } + + private void onLogin() + { + loadConfig(); + resetConfig(); + + WorldPoint newLocation = client.getLocalPlayer().getWorldLocation(); + assert newLocation != null; + + // If the player isn't at the location he/she logged out at, + // the safe unaggro area probably changed, and should be disposed. + if (lastPlayerLocation == null || newLocation.distanceTo(lastPlayerLocation) != 0) + { + safeCenters[0] = null; + safeCenters[1] = null; + lastPlayerLocation = newLocation; + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case LOGGED_IN: + if (loggingIn) + { + loggingIn = false; + onLogin(); + } + + recheckActive(); + break; + + case LOGGING_IN: + loggingIn = true; + break; + + case LOGIN_SCREEN: + if (lastPlayerLocation != null) + { + saveConfig(); + } + + safeCenters[0] = null; + safeCenters[1] = null; + lastPlayerLocation = null; + break; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ColorTileObject.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ColorTileObject.java new file mode 100644 index 0000000000..1af342cd20 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ColorTileObject.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020, dekvall + * 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.objectindicators; + +import java.awt.Color; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import net.runelite.api.TileObject; + +/** + * Used to denote marked objects and their colors. + * Note: This is not used for serialization of object indicators; see {@link ObjectPoint} + */ +@Value +@RequiredArgsConstructor +class ColorTileObject +{ + private final TileObject tileObject; + private final Color color; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java new file mode 100644 index 0000000000..049269756b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.objectindicators; + +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; + +@ConfigGroup("objectindicators") +public interface ObjectIndicatorsConfig extends Config +{ + @Alpha + @ConfigItem( + keyName = "markerColor", + name = "Marker color", + description = "Configures the color of object marker" + ) + default Color markerColor() + { + return Color.YELLOW; + } + + @ConfigItem( + keyName = "rememberObjectColors", + name = "Remember color per object", + description = "Color objects using the color from time of marking" + ) + default boolean rememberObjectColors() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java new file mode 100644 index 0000000000..0e52edf0a4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.objectindicators; + +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.DecorativeObject; +import net.runelite.api.GameObject; +import net.runelite.api.GroundObject; +import net.runelite.api.TileObject; +import net.runelite.api.WallObject; +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.OverlayPriority; +import net.runelite.client.ui.overlay.OverlayUtil; + +class ObjectIndicatorsOverlay extends Overlay +{ + private final Client client; + private final ObjectIndicatorsConfig config; + private final ObjectIndicatorsPlugin plugin; + + @Inject + private ObjectIndicatorsOverlay(Client client, ObjectIndicatorsConfig config, ObjectIndicatorsPlugin plugin) + { + this.client = client; + this.config = config; + this.plugin = plugin; + setPosition(OverlayPosition.DYNAMIC); + setPriority(OverlayPriority.LOW); + setLayer(OverlayLayer.ABOVE_SCENE); + } + + @Override + public Dimension render(Graphics2D graphics) + { + for (ColorTileObject colorTileObject : plugin.getObjects()) + { + TileObject object = colorTileObject.getTileObject(); + Color color = colorTileObject.getColor(); + + if (object.getPlane() != client.getPlane()) + { + continue; + } + + if (color == null || !config.rememberObjectColors()) + { + // Fallback to the current config if the object is marked before the addition of multiple colors + color = config.markerColor(); + } + + final Shape polygon; + Shape polygon2 = null; + + if (object instanceof GameObject) + { + polygon = ((GameObject) object).getConvexHull(); + } + else if (object instanceof WallObject) + { + polygon = ((WallObject) object).getConvexHull(); + polygon2 = ((WallObject) object).getConvexHull2(); + } + else if (object instanceof DecorativeObject) + { + polygon = ((DecorativeObject) object).getConvexHull(); + polygon2 = ((DecorativeObject) object).getConvexHull2(); + } + else if (object instanceof GroundObject) + { + polygon = ((GroundObject) object).getConvexHull(); + } + else + { + polygon = object.getCanvasTilePoly(); + } + + if (polygon != null) + { + OverlayUtil.renderPolygon(graphics, polygon, color); + } + + if (polygon2 != null) + { + OverlayUtil.renderPolygon(graphics, polygon2, color); + } + } + + return null; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java new file mode 100644 index 0000000000..46a4a0924d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.objectindicators; + +import com.google.common.base.Strings; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.google.inject.Provides; +import java.awt.Color; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.DecorativeObject; +import net.runelite.api.GameObject; +import net.runelite.api.GameState; +import net.runelite.api.GroundObject; +import net.runelite.api.KeyCode; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.ObjectComposition; +import net.runelite.api.Scene; +import net.runelite.api.Tile; +import net.runelite.api.TileObject; +import net.runelite.api.WallObject; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.DecorativeObjectDespawned; +import net.runelite.api.events.DecorativeObjectSpawned; +import net.runelite.api.events.GameObjectDespawned; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GroundObjectDespawned; +import net.runelite.api.events.GroundObjectSpawned; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.events.WallObjectChanged; +import net.runelite.api.events.WallObjectDespawned; +import net.runelite.api.events.WallObjectSpawned; +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 = "Object Markers", + description = "Enable marking of objects using the Shift key", + tags = {"overlay", "objects", "mark", "marker"}, + enabledByDefault = false +) +@Slf4j +public class ObjectIndicatorsPlugin extends Plugin +{ + private static final String CONFIG_GROUP = "objectindicators"; + private static final String MARK = "Mark object"; + private static final String UNMARK = "Unmark object"; + + private final Gson GSON = new Gson(); + @Getter(AccessLevel.PACKAGE) + private final List objects = new ArrayList<>(); + private final Map> points = new HashMap<>(); + + @Inject + private Client client; + + @Inject + private ConfigManager configManager; + + @Inject + private OverlayManager overlayManager; + + @Inject + private ObjectIndicatorsOverlay overlay; + + @Inject + private ObjectIndicatorsConfig config; + + @Provides + ObjectIndicatorsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(ObjectIndicatorsConfig.class); + } + + @Override + protected void startUp() + { + overlayManager.add(overlay); + } + + @Override + protected void shutDown() + { + overlayManager.remove(overlay); + points.clear(); + objects.clear(); + } + + @Subscribe + public void onWallObjectSpawned(WallObjectSpawned event) + { + checkObjectPoints(event.getWallObject()); + } + + @Subscribe + public void onWallObjectChanged(WallObjectChanged event) + { + WallObject previous = event.getPrevious(); + WallObject wallObject = event.getWallObject(); + + objects.removeIf(o -> o.getTileObject() == previous); + checkObjectPoints(wallObject); + } + + @Subscribe + public void onWallObjectDespawned(WallObjectDespawned event) + { + objects.removeIf(o -> o.getTileObject() == event.getWallObject()); + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned event) + { + checkObjectPoints(event.getGameObject()); + } + + @Subscribe + public void onDecorativeObjectSpawned(DecorativeObjectSpawned event) + { + checkObjectPoints(event.getDecorativeObject()); + } + + @Subscribe + public void onGameObjectDespawned(GameObjectDespawned event) + { + objects.removeIf(o -> o.getTileObject() == event.getGameObject()); + } + + @Subscribe + public void onDecorativeObjectDespawned(DecorativeObjectDespawned event) + { + objects.removeIf(o -> o.getTileObject() == event.getDecorativeObject()); + } + + @Subscribe + public void onGroundObjectSpawned(GroundObjectSpawned event) + { + checkObjectPoints(event.getGroundObject()); + } + + @Subscribe + public void onGroundObjectDespawned(GroundObjectDespawned event) + { + objects.removeIf(o -> o.getTileObject() == event.getGroundObject()); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged gameStateChanged) + { + GameState gameState = gameStateChanged.getGameState(); + if (gameState == GameState.LOADING) + { + // Reload points with new map regions + + points.clear(); + for (int regionId : client.getMapRegions()) + { + // load points for region + final Set regionPoints = loadPoints(regionId); + if (regionPoints != null) + { + points.put(regionId, regionPoints); + } + } + } + + if (gameStateChanged.getGameState() != GameState.LOGGED_IN) + { + objects.clear(); + } + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded event) + { + if (event.getType() != MenuAction.EXAMINE_OBJECT.getId() || !client.isKeyPressed(KeyCode.KC_SHIFT)) + { + return; + } + + final Tile tile = client.getScene().getTiles()[client.getPlane()][event.getActionParam0()][event.getActionParam1()]; + final TileObject tileObject = findTileObject(tile, event.getIdentifier()); + + if (tileObject == null) + { + return; + } + + MenuEntry[] menuEntries = client.getMenuEntries(); + menuEntries = Arrays.copyOf(menuEntries, menuEntries.length + 1); + MenuEntry menuEntry = menuEntries[menuEntries.length - 1] = new MenuEntry(); + menuEntry.setOption(objects.stream().anyMatch(o -> o.getTileObject() == tileObject) ? UNMARK : MARK); + menuEntry.setTarget(event.getTarget()); + menuEntry.setParam0(event.getActionParam0()); + menuEntry.setParam1(event.getActionParam1()); + menuEntry.setIdentifier(event.getIdentifier()); + menuEntry.setType(MenuAction.RUNELITE.getId()); + client.setMenuEntries(menuEntries); + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked event) + { + if (event.getMenuAction() != MenuAction.RUNELITE + || !(event.getMenuOption().equals(MARK) || event.getMenuOption().equals(UNMARK))) + { + return; + } + + Scene scene = client.getScene(); + Tile[][][] tiles = scene.getTiles(); + final int x = event.getActionParam(); + final int y = event.getWidgetId(); + final int z = client.getPlane(); + final Tile tile = tiles[z][x][y]; + + TileObject object = findTileObject(tile, event.getId()); + if (object == null) + { + return; + } + + // object.getId() is always the base object id, getObjectComposition transforms it to + // the correct object we see + ObjectComposition objectDefinition = getObjectComposition(object.getId()); + String name = objectDefinition.getName(); + // Name is probably never "null" - however prevent adding it if it is, as it will + // become ambiguous as objects with no name are assigned name "null" + if (Strings.isNullOrEmpty(name) || name.equals("null")) + { + return; + } + + markObject(objectDefinition, name, object); + } + + private void checkObjectPoints(TileObject object) + { + final WorldPoint worldPoint = WorldPoint.fromLocalInstance(client, object.getLocalLocation(), object.getPlane()); + final Set objectPoints = points.get(worldPoint.getRegionID()); + + if (objectPoints == null) + { + return; + } + + for (ObjectPoint objectPoint : objectPoints) + { + if (worldPoint.getRegionX() == objectPoint.getRegionX() + && worldPoint.getRegionY() == objectPoint.getRegionY() + && worldPoint.getPlane() == objectPoint.getZ()) + { + // Transform object to get the name which matches against what we've stored + ObjectComposition composition = getObjectComposition(object.getId()); + if (composition != null && objectPoint.getName().equals(composition.getName())) + { + log.debug("Marking object {} due to matching {}", object, objectPoint); + objects.add(new ColorTileObject(object, objectPoint.getColor())); + break; + } + } + } + } + + private TileObject findTileObject(Tile tile, int id) + { + if (tile == null) + { + return null; + } + + final GameObject[] tileGameObjects = tile.getGameObjects(); + final DecorativeObject tileDecorativeObject = tile.getDecorativeObject(); + final WallObject tileWallObject = tile.getWallObject(); + final GroundObject groundObject = tile.getGroundObject(); + + if (objectIdEquals(tileWallObject, id)) + { + return tileWallObject; + } + + if (objectIdEquals(tileDecorativeObject, id)) + { + return tileDecorativeObject; + } + + if (objectIdEquals(groundObject, id)) + { + return groundObject; + } + + for (GameObject object : tileGameObjects) + { + if (objectIdEquals(object, id)) + { + return object; + } + } + + return null; + } + + private boolean objectIdEquals(TileObject tileObject, int id) + { + if (tileObject == null) + { + return false; + } + + if (tileObject.getId() == id) + { + return true; + } + + // Menu action EXAMINE_OBJECT sends the transformed object id, not the base id, unlike + // all of the GAME_OBJECT_OPTION actions, so check the id against the impostor ids + final ObjectComposition comp = client.getObjectDefinition(tileObject.getId()); + + if (comp.getImpostorIds() != null) + { + for (int impostorId : comp.getImpostorIds()) + { + if (impostorId == id) + { + return true; + } + } + } + + return false; + } + + /** mark or unmark an object + * + * @param objectComposition transformed composition of object based on vars + * @param name name of objectComposition + * @param object tile object, for multilocs object.getId() is the base id + */ + private void markObject(ObjectComposition objectComposition, String name, final TileObject object) + { + final WorldPoint worldPoint = WorldPoint.fromLocalInstance(client, object.getLocalLocation()); + final int regionId = worldPoint.getRegionID(); + final Color color = config.markerColor(); + final ObjectPoint point = new ObjectPoint( + object.getId(), + name, + regionId, + worldPoint.getRegionX(), + worldPoint.getRegionY(), + worldPoint.getPlane(), + color); + + Set objectPoints = points.computeIfAbsent(regionId, k -> new HashSet<>()); + + if (objects.removeIf(o -> o.getTileObject() == object)) + { + // Find the object point that caused this object to be marked, there are two cases: + // 1) object is a multiloc, the name may have changed since marking - match from base id + // 2) not a multiloc, but an object has spawned with an identical name and a different + // id as what was originally marked + if (!objectPoints.removeIf(op -> ((op.getId() == -1 || op.getId() == object.getId()) || op.getName().equals(objectComposition.getName())) + && op.getRegionX() == worldPoint.getRegionX() + && op.getRegionY() == worldPoint.getRegionY() + && op.getZ() == worldPoint.getPlane())) + { + log.warn("unable to find object point for unmarked object {}", object.getId()); + } + + log.debug("Unmarking object: {}", point); + } + else + { + objectPoints.add(point); + objects.add(new ColorTileObject(object, color)); + log.debug("Marking object: {}", point); + } + + savePoints(regionId, objectPoints); + } + + private void savePoints(final int id, final Set points) + { + if (points.isEmpty()) + { + configManager.unsetConfiguration(CONFIG_GROUP, "region_" + id); + } + else + { + final String json = GSON.toJson(points); + configManager.setConfiguration(CONFIG_GROUP, "region_" + id, json); + } + } + + private Set loadPoints(final int id) + { + final String json = configManager.getConfiguration(CONFIG_GROUP, "region_" + id); + + if (Strings.isNullOrEmpty(json)) + { + return null; + } + + Set points = GSON.fromJson(json, new TypeToken>() + { + }.getType()); + // Prior to multiloc support the plugin would mark objects named "null", which breaks + // in most cases due to the specific object being identified being ambiguous, so remove + // them + return points.stream() + .filter(point -> !point.getName().equals("null")) + .collect(Collectors.toSet()); + } + + @Nullable + private ObjectComposition getObjectComposition(int id) + { + ObjectComposition objectComposition = client.getObjectDefinition(id); + return objectComposition.getImpostorIds() == null ? objectComposition : objectComposition.getImpostor(); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectPoint.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectPoint.java new file mode 100644 index 0000000000..6064eb2118 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectPoint.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.objectindicators; + +import java.awt.Color; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +class ObjectPoint +{ + private int id = -1; + private String name; + private int regionId; + private int regionX; + private int regionY; + private int z; + private Color color; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java index 4051d5e509..a2dfb8577b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java @@ -45,7 +45,7 @@ import static net.runelite.api.widgets.WidgetInfo.LIGHT_BOX_BUTTON_E; import static net.runelite.api.widgets.WidgetInfo.LIGHT_BOX_BUTTON_F; import static net.runelite.api.widgets.WidgetInfo.LIGHT_BOX_BUTTON_G; import static net.runelite.api.widgets.WidgetInfo.LIGHT_BOX_BUTTON_H; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.plugins.Plugin; @@ -139,7 +139,7 @@ public class PuzzleSolverPlugin extends Plugin public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked) { int widgetId = menuOptionClicked.getWidgetId(); - if (getGroupFromID(widgetId) != WidgetID.LIGHT_BOX_GROUP_ID) + if (TO_GROUP(widgetId) != WidgetID.LIGHT_BOX_GROUP_ID) { return; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ScreenMarkerWidgetHighlightOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ScreenMarkerWidgetHighlightOverlay.java index e58c7644f7..09c08af6f0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ScreenMarkerWidgetHighlightOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ScreenMarkerWidgetHighlightOverlay.java @@ -74,7 +74,7 @@ class ScreenMarkerWidgetHighlightOverlay extends Overlay final MenuEntry menuEntry = menuEntries[menuEntries.length - 1]; final int childIdx = menuEntry.getParam0(); final int widgetId = menuEntry.getParam1(); - final int groupId = WidgetInfo.getGroupFromID(widgetId); + final int groupId = WidgetInfo.TO_GROUP(widgetId); final int componentId = WidgetInfo.getChildFromID(widgetId); final Widget widget = client.getWidget(groupId, componentId); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java index 3b005bfb21..ec45039942 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java @@ -279,7 +279,7 @@ public class TimersPlugin extends Plugin { Widget widget = event.getWidget(); if (WorldType.isPvpWorld(client.getWorldType()) - && WidgetInfo.getGroupFromID(widget.getId()) == WidgetID.PVP_GROUP_ID) + && WidgetInfo.TO_GROUP(widget.getId()) == WidgetID.PVP_GROUP_ID) { widgetHiddenChangedOnPvpWorld = true; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java index 6f72d10e5a..9ae532a8db 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java @@ -58,7 +58,7 @@ import net.runelite.api.events.MenuOptionClicked; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.StatChanged; import net.runelite.api.widgets.WidgetID; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.game.NPCManager; @@ -483,7 +483,7 @@ public class XpTrackerPlugin extends Plugin { int widgetID = event.getActionParam1(); - if (getGroupFromID(widgetID) != WidgetID.SKILLS_GROUP_ID + if (TO_GROUP(widgetID) != WidgetID.SKILLS_GROUP_ID || !event.getOption().startsWith("View") || !xpTrackerConfig.skillTabOverlayMenuOptions()) { @@ -511,7 +511,7 @@ public class XpTrackerPlugin extends Plugin public void onMenuOptionClicked(MenuOptionClicked event) { if (event.getMenuAction().getId() != MenuAction.RUNELITE.getId() - || getGroupFromID(event.getWidgetId()) != WidgetID.SKILLS_GROUP_ID) + || TO_GROUP(event.getWidgetId()) != WidgetID.SKILLS_GROUP_ID) { return; } diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/WidgetItemOverlay.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/WidgetItemOverlay.java index f3a1abd1e5..295c1d618b 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/WidgetItemOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/WidgetItemOverlay.java @@ -50,7 +50,7 @@ import static net.runelite.api.widgets.WidgetID.PLAYER_TRADE_SCREEN_GROUP_ID; import static net.runelite.api.widgets.WidgetID.PLAYER_TRADE_INVENTORY_GROUP_ID; import static net.runelite.api.widgets.WidgetInfo.BANK_CONTENT_CONTAINER; import static net.runelite.api.widgets.WidgetInfo.BANK_TAB_CONTAINER; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; import net.runelite.api.widgets.WidgetItem; public abstract class WidgetItemOverlay extends Overlay @@ -80,7 +80,7 @@ public abstract class WidgetItemOverlay extends Overlay for (WidgetItem widgetItem : itemWidgets) { Widget widget = widgetItem.getWidget(); - int interfaceGroup = getGroupFromID(widget.getId()); + int interfaceGroup = TO_GROUP(widget.getId()); // Don't draw if this widget isn't one of the allowed nor in tag tab/item tab if (!interfaceGroups.contains(interfaceGroup) || diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java index b8f417e334..8893d234df 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java @@ -594,7 +594,7 @@ public abstract class RSClientMixin implements RSClient @Override public Widget getWidget(int id) { - return getWidget(WidgetInfo.getGroupFromID(id), WidgetInfo.getChildFromID(id)); + return getWidget(WidgetInfo.TO_GROUP(id), WidgetInfo.getChildFromID(id)); } @Inject diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSWidgetMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSWidgetMixin.java index 3652bc6449..837c7fef53 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSWidgetMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSWidgetMixin.java @@ -51,7 +51,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import static net.runelite.api.widgets.WidgetInfo.getChildFromID; -import static net.runelite.api.widgets.WidgetInfo.getGroupFromID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; @Mixin(RSWidget.class) public abstract class RSWidgetMixin implements RSWidget @@ -107,7 +107,7 @@ public abstract class RSWidgetMixin implements RSWidget return null; } - return client.getWidget(getGroupFromID(id), getChildFromID(id)); + return client.getWidget(TO_GROUP(id), getChildFromID(id)); } @Inject @@ -123,7 +123,7 @@ public abstract class RSWidgetMixin implements RSWidget } final int id = getId(); - if (getGroupFromID(id) == client.getWidgetRoot()) + if (TO_GROUP(id) == client.getWidgetRoot()) { // this is a root widget return -1; @@ -140,7 +140,7 @@ public abstract class RSWidgetMixin implements RSWidget // check the parent in the component table @SuppressWarnings("unchecked") HashTable componentTable = client.getComponentTable(); WidgetNode widgetNode = componentTable.get(parentId); - if (widgetNode == null || widgetNode.getId() != getGroupFromID(id)) + if (widgetNode == null || widgetNode.getId() != TO_GROUP(id)) { // invalidate parent rl$parentId = -1; @@ -152,7 +152,7 @@ public abstract class RSWidgetMixin implements RSWidget } // also the widget may not have been drawn, yet - int groupId = getGroupFromID(getId()); + int groupId = TO_GROUP(getId()); RSNodeHashTable componentTable = client.getComponentTable(); RSNode[] buckets = componentTable.getBuckets(); for (RSNode node : buckets) @@ -220,7 +220,7 @@ public abstract class RSWidgetMixin implements RSWidget // If the parent is hidden, this widget is also hidden. // Widget has no parent and is not the root widget (which is always visible), // so it's not visible. - return parent == null ? getGroupFromID(getId()) != client.getWidgetRoot() : parent.isHidden(); + return parent == null ? TO_GROUP(getId()) != client.getWidgetRoot() : parent.isHidden(); } @Inject @@ -360,7 +360,7 @@ public abstract class RSWidgetMixin implements RSWidget } List widgets = new ArrayList(); - for (RSWidget widget : client.getGroup(getGroupFromID(getId()))) + for (RSWidget widget : client.getGroup(TO_GROUP(getId()))) { if (widget != null && widget.getRSParentId() == getId()) { @@ -475,7 +475,7 @@ public abstract class RSWidgetMixin implements RSWidget return; } } - else if (getGroupFromID(id) != client.getWidgetRoot()) + else if (TO_GROUP(id) != client.getWidgetRoot()) { return; } @@ -576,7 +576,7 @@ public abstract class RSWidgetMixin implements RSWidget assert client.isClientThread(); client.revalidateWidget(this); - client.revalidateWidgetScroll(client.getWidgets()[getGroupFromID(this.getId())], this, false); + client.revalidateWidgetScroll(client.getWidgets()[TO_GROUP(this.getId())], this, false); } @Inject diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/SoundEffectMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/SoundEffectMixin.java index c03f57e08a..c23c7cc483 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/SoundEffectMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/SoundEffectMixin.java @@ -173,7 +173,7 @@ public abstract class SoundEffectMixin implements RSClient } // If the current volume is not muted, use it instead - final int soundEffectVolume = client.getPreferences().getSoundEffectsVolume(); + final int soundEffectVolume = client.getPreferences().getSoundEffectVolume(); if (soundEffectVolume != SoundEffectVolume.MUTED) { volume = soundEffectVolume; diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java index f1513802cb..e798354db7 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java @@ -849,7 +849,7 @@ public interface RSClient extends RSGameShell, Client @Import("ItemDefinition_cached") @Override - RSEvictingDualNodeHashTable getItemDefinitionCache(); + RSEvictingDualNodeHashTable getItemCompositionCache(); @Import("oculusOrbState") @Override diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSClientPreferences.java b/runescape-api/src/main/java/net/runelite/rs/api/RSClientPreferences.java index aa822f4af6..edb36c8e72 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSClientPreferences.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSClientPreferences.java @@ -15,19 +15,19 @@ public interface RSClientPreferences extends Preferences @Import("soundEffectsVolume") @Override - int getSoundEffectsVolume(); + int getSoundEffectVolume(); @Import("soundEffectsVolume") @Override - void setSoundEffectsVolume(int i); + void setSoundEffectVolume(int i); @Import("areaSoundEffectsVolume") @Override - int getAreaSoundEffectsVolume(); + int getAreaSoundEffectVolume(); @Import("areaSoundEffectsVolume") @Override - void setAreaSoundEffectsVolume(int i); + void setAreaSoundEffectVolume(int i); @Import("musicVolume") @Override