From e276d5e5cee9d43d8dabeae6d8f326759b0d194f Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 29 Jul 2021 18:06:24 -0400 Subject: [PATCH] Add interact highlight plugin Co-authored-by: Eirik Leikvoll <12532870+LeikvollE@users.noreply.github.com> --- .../InteractHighlightConfig.java | 202 +++++++++++++++ .../InteractHighlightOverlay.java | 158 ++++++++++++ .../InteractHighlightPlugin.java | 230 ++++++++++++++++++ .../net/runelite/client/util/ColorUtil.java | 5 +- .../runelite/client/util/ColorUtilTest.java | 1 + 5 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightConfig.java new file mode 100644 index 0000000000..8fff918cda --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightConfig.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021, 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.interacthighlight; + +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; +import net.runelite.client.config.Range; + +@ConfigGroup("interacthighlight") +public interface InteractHighlightConfig extends Config +{ + @ConfigSection( + name = "NPCs", + description = "Settings for NPC highlight", + position = 0 + ) + String npcSection = "npcSection"; + + @ConfigSection( + name = "Objects", + description = "Settings for object highlight", + position = 1 + ) + String objectSection = "objectSection"; + + @ConfigItem( + keyName = "npcShowHover", + name = "Show on hover", + description = "Outline NPCs when hovered", + position = 1, + section = npcSection + ) + default boolean npcShowHover() + { + return true; + } + + @ConfigItem( + keyName = "npcShowInteract", + name = "Show on interact", + description = "Outline NPCs when interacted", + position = 2, + section = npcSection + ) + default boolean npcShowInteract() + { + return true; + } + + @Alpha + @ConfigItem( + keyName = "npcHoverHighlightColor", + name = "NPC hover", + description = "The color of the hover outline for NPCs", + position = 3, + section = npcSection + ) + default Color npcHoverHighlightColor() + { + return new Color(0x90FFFF00, true); + } + + @Alpha + @ConfigItem( + keyName = "npcAttackHoverHighlightColor", + name = "NPC attack hover", + description = "The color of the attack hover outline for NPCs", + position = 4, + section = npcSection + ) + default Color npcAttackHoverHighlightColor() + { + return new Color(0x90FFFF00, true); + } + + @Alpha + @ConfigItem( + keyName = "npcInteractHighlightColor", + name = "NPC interact", + description = "The color of the target outline for NPCs", + position = 5, + section = npcSection + ) + default Color npcInteractHighlightColor() + { + return new Color(0x90FF0000, true); + } + + @Alpha + @ConfigItem( + keyName = "npcAttackHighlightColor", + name = "NPC attack", + description = "The color of the outline on attacked NPCs", + position = 6, + section = npcSection + ) + default Color npcAttackHighlightColor() + { + return new Color(0x90FF0000, true); + } + + @ConfigItem( + keyName = "objectShowHover", + name = "Show on hover", + description = "Outline objects when hovered", + position = 1, + section = objectSection + ) + default boolean objectShowHover() + { + return true; + } + + @ConfigItem( + keyName = "objectShowInteract", + name = "Show on interact", + description = "Outline objects when interacted", + position = 2, + section = objectSection + ) + default boolean objectShowInteract() + { + return true; + } + + @Alpha + @ConfigItem( + keyName = "objectHoverHighlightColor", + name = "Object hover", + description = "The color of the hover outline for objects", + position = 4, + section = objectSection + ) + default Color objectHoverHighlightColor() + { + return new Color(0x9000FFFF, true); + } + + @Alpha + @ConfigItem( + keyName = "objectInteractHighlightColor", + name = "Object interact", + description = "The color of the target outline for objects", + position = 6, + section = objectSection + ) + default Color objectInteractHighlightColor() + { + return new Color(0x90FF0000, true); + } + + @ConfigItem( + keyName = "borderWidth", + name = "Border Width", + description = "Width of the outlined border", + position = 7 + ) + default int borderWidth() + { + return 4; + } + + @ConfigItem( + keyName = "outlineFeather", + name = "Outline feather", + description = "Specify between 0-4 how much of the model outline should be faded", + position = 8 + ) + @Range( + max = 4 + ) + default int outlineFeather() + { + return 4; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightOverlay.java new file mode 100644 index 0000000000..664d274f5a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightOverlay.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2021, 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.interacthighlight; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.NPC; +import net.runelite.api.TileObject; +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.outline.ModelOutlineRenderer; +import net.runelite.client.util.ColorUtil; + +class InteractHighlightOverlay extends Overlay +{ + private static final Color INTERACT_CLICK_COLOR = new Color(0x90ffffff); + + private final Client client; + private final InteractHighlightPlugin plugin; + private final InteractHighlightConfig config; + private final ModelOutlineRenderer modelOutlineRenderer; + + @Inject + private InteractHighlightOverlay(Client client, InteractHighlightPlugin plugin, InteractHighlightConfig config, ModelOutlineRenderer modelOutlineRenderer) + { + this.client = client; + this.plugin = plugin; + this.config = config; + this.modelOutlineRenderer = modelOutlineRenderer; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + setPriority(OverlayPriority.LOW); + } + + @Override + public Dimension render(Graphics2D graphics) + { + renderMouseover(); + renderTarget(); + return null; + } + + private void renderMouseover() + { + MenuEntry[] menuEntries = client.getMenuEntries(); + if (menuEntries.length == 0) + { + return; + } + + MenuEntry top = menuEntries[menuEntries.length - 1]; + MenuAction menuAction = MenuAction.of(top.getType()); + + switch (menuAction) + { + case ITEM_USE_ON_GAME_OBJECT: + case SPELL_CAST_ON_GAME_OBJECT: + case GAME_OBJECT_FIRST_OPTION: + case GAME_OBJECT_SECOND_OPTION: + case GAME_OBJECT_THIRD_OPTION: + case GAME_OBJECT_FOURTH_OPTION: + case GAME_OBJECT_FIFTH_OPTION: + { + int x = top.getParam0(); + int y = top.getParam1(); + int id = top.getIdentifier(); + TileObject tileObject = plugin.findTileObject(x, y, id); + if (tileObject != null && config.objectShowHover() && (tileObject != plugin.getInteractedObject() || !config.objectShowInteract())) + { + modelOutlineRenderer.drawOutline(tileObject, config.borderWidth(), config.objectHoverHighlightColor(), config.outlineFeather()); + } + break; + } + case ITEM_USE_ON_NPC: + case SPELL_CAST_ON_NPC: + case NPC_FIRST_OPTION: + case NPC_SECOND_OPTION: + case NPC_THIRD_OPTION: + case NPC_FOURTH_OPTION: + case NPC_FIFTH_OPTION: + { + int id = top.getIdentifier(); + NPC npc = plugin.findNpc(id); + if (npc != null && config.npcShowHover() && (npc != plugin.getInteractedTarget() || !config.npcShowInteract())) + { + Color highlightColor = menuAction == MenuAction.NPC_SECOND_OPTION || menuAction == MenuAction.SPELL_CAST_ON_NPC + ? config.npcAttackHoverHighlightColor() : config.npcHoverHighlightColor(); + modelOutlineRenderer.drawOutline(npc, config.borderWidth(), highlightColor, config.outlineFeather()); + } + break; + } + } + } + + private void renderTarget() + { + TileObject interactedObject = plugin.getInteractedObject(); + if (interactedObject != null && config.objectShowInteract()) + { + Color clickColor = getClickColor(config.objectHoverHighlightColor(), config.objectInteractHighlightColor(), + client.getGameCycle() - plugin.getGameCycle()); + modelOutlineRenderer.drawOutline(interactedObject, config.borderWidth(), clickColor, config.outlineFeather()); + } + + Actor target = plugin.getInteractedTarget(); + if (target instanceof NPC && config.npcShowInteract()) + { + Color startColor = plugin.isAttacked() ? config.npcAttackHoverHighlightColor() : config.npcHoverHighlightColor(); + Color endColor = plugin.isAttacked() ? config.npcAttackHighlightColor() : config.npcInteractHighlightColor(); + Color clickColor = getClickColor(startColor, endColor, + client.getGameCycle() - plugin.getGameCycle()); + modelOutlineRenderer.drawOutline((NPC) target, config.borderWidth(), clickColor, config.outlineFeather()); + } + } + + private Color getClickColor(Color start, Color end, long time) + { + if (time < 5) + { + return ColorUtil.colorLerp(start, INTERACT_CLICK_COLOR, time / 5f); + } + else if (time < 10) + { + return ColorUtil.colorLerp(INTERACT_CLICK_COLOR, end, (time - 5) / 5f); + } + return end; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java new file mode 100644 index 0000000000..96ef8f1ba5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2021, 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.interacthighlight; + +import com.google.inject.Provides; +import javax.annotation.Nullable; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.api.Actor; +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.MenuAction; +import net.runelite.api.NPC; +import net.runelite.api.Scene; +import net.runelite.api.Tile; +import net.runelite.api.TileObject; +import net.runelite.api.WallObject; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.events.NpcDespawned; +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 = "Interact Highlight", + description = "Outlines npcs and objects you interact with or hover over", + enabledByDefault = false +) +public class InteractHighlightPlugin extends Plugin +{ + @Inject + private OverlayManager overlayManager; + + @Inject + private InteractHighlightOverlay interactHighlightOverlay; + + @Inject + private Client client; + + @Getter(AccessLevel.PACKAGE) + private TileObject interactedObject; + private NPC interactedNpc; + @Getter(AccessLevel.PACKAGE) + boolean attacked; + private int clickTick; + @Getter(AccessLevel.PACKAGE) + private int gameCycle; + + @Provides + InteractHighlightConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(InteractHighlightConfig.class); + } + + @Override + protected void startUp() + { + overlayManager.add(interactHighlightOverlay); + } + + @Override + protected void shutDown() + { + overlayManager.remove(interactHighlightOverlay); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged gameStateChanged) + { + if (gameStateChanged.getGameState() == GameState.LOADING) + { + interactedObject = null; + } + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + if (npcDespawned.getNpc() == interactedNpc) + { + interactedNpc = null; + } + } + + @Subscribe + public void onGameTick(GameTick gameTick) + { + if (client.getTickCount() > clickTick && client.getLocalDestinationLocation() == null) + { + // when the destination is reached, clear the interacting object + interactedObject = null; + interactedNpc = null; + } + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked) + { + switch (menuOptionClicked.getMenuAction()) + { + case ITEM_USE_ON_GAME_OBJECT: + case SPELL_CAST_ON_GAME_OBJECT: + case GAME_OBJECT_FIRST_OPTION: + case GAME_OBJECT_SECOND_OPTION: + case GAME_OBJECT_THIRD_OPTION: + case GAME_OBJECT_FOURTH_OPTION: + case GAME_OBJECT_FIFTH_OPTION: + { + int x = menuOptionClicked.getParam0(); + int y = menuOptionClicked.getParam1(); + int id = menuOptionClicked.getId(); + interactedObject = findTileObject(x, y, id); + interactedNpc = null; + clickTick = client.getTickCount(); + gameCycle = client.getGameCycle(); + break; + } + case ITEM_USE_ON_NPC: + case SPELL_CAST_ON_NPC: + case NPC_FIRST_OPTION: + case NPC_SECOND_OPTION: + case NPC_THIRD_OPTION: + case NPC_FOURTH_OPTION: + case NPC_FIFTH_OPTION: + { + int id = menuOptionClicked.getId(); + interactedObject = null; + interactedNpc = findNpc(id); + attacked = menuOptionClicked.getMenuAction() == MenuAction.NPC_SECOND_OPTION || menuOptionClicked.getMenuAction() == MenuAction.SPELL_CAST_ON_NPC; + clickTick = client.getTickCount(); + gameCycle = client.getGameCycle(); + break; + } + // Any menu click which clears an interaction + case WALK: + case ITEM_USE: + case ITEM_USE_ON_GROUND_ITEM: + case ITEM_USE_ON_PLAYER: + case ITEM_FIRST_OPTION: + case ITEM_SECOND_OPTION: + case ITEM_THIRD_OPTION: + case ITEM_FOURTH_OPTION: + case ITEM_FIFTH_OPTION: + case GROUND_ITEM_FIRST_OPTION: + case GROUND_ITEM_SECOND_OPTION: + case GROUND_ITEM_THIRD_OPTION: + case GROUND_ITEM_FOURTH_OPTION: + case GROUND_ITEM_FIFTH_OPTION: + interactedObject = null; + interactedNpc = null; + } + } + + TileObject findTileObject(int x, int y, int id) + { + Scene scene = client.getScene(); + Tile[][][] tiles = scene.getTiles(); + Tile tile = tiles[client.getPlane()][x][y]; + if (tile != null) + { + for (GameObject gameObject : tile.getGameObjects()) + { + if (gameObject != null && gameObject.getId() == id) + { + return gameObject; + } + } + + WallObject wallObject = tile.getWallObject(); + if (wallObject != null && wallObject.getId() == id) + { + return wallObject; + } + + DecorativeObject decorativeObject = tile.getDecorativeObject(); + if (decorativeObject != null && decorativeObject.getId() == id) + { + return decorativeObject; + } + + GroundObject groundObject = tile.getGroundObject(); + if (groundObject != null && groundObject.getId() == id) + { + return groundObject; + } + } + return null; + } + + NPC findNpc(int id) + { + return client.getCachedNPCs()[id]; + } + + @Nullable + Actor getInteractedTarget() + { + return interactedNpc != null ? interactedNpc : client.getLocalPlayer().getInteracting(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/util/ColorUtil.java b/runelite-client/src/main/java/net/runelite/client/util/ColorUtil.java index 6b39003f11..6a7b8f8908 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/ColorUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/util/ColorUtil.java @@ -101,11 +101,14 @@ public class ColorUtil final double g2 = b.getGreen(); final double b1 = a.getBlue(); final double b2 = b.getBlue(); + final double a1 = a.getAlpha(); + final double a2 = b.getAlpha(); return new Color( (int) Math.round(r1 + (t * (r2 - r1))), (int) Math.round(g1 + (t * (g2 - g1))), - (int) Math.round(b1 + (t * (b2 - b1))) + (int) Math.round(b1 + (t * (b2 - b1))), + (int) Math.round(a1 + (t * (a2 - a1))) ); } diff --git a/runelite-client/src/test/java/net/runelite/client/util/ColorUtilTest.java b/runelite-client/src/test/java/net/runelite/client/util/ColorUtilTest.java index 8a531b18bc..cf663d1fa2 100644 --- a/runelite-client/src/test/java/net/runelite/client/util/ColorUtilTest.java +++ b/runelite-client/src/test/java/net/runelite/client/util/ColorUtilTest.java @@ -116,6 +116,7 @@ public class ColorUtilTest assertEquals(new Color(128, 128, 128), ColorUtil.colorLerp(Color.BLACK, Color.WHITE, 0.5)); assertEquals(Color.BLACK, ColorUtil.colorLerp(Color.BLACK, Color.CYAN, 0)); assertEquals(Color.CYAN, ColorUtil.colorLerp(Color.BLACK, Color.CYAN, 1)); + assertEquals(new Color(0x80800080, true), ColorUtil.colorLerp(new Color(0xff0000ff, true), new Color(0x00ff0000, true), 0.5)); } @Test