From 82d92b9953aae37d1e81dc1ff3815cd188f1a7a0 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 8 Jun 2022 14:19:22 -0400 Subject: [PATCH] Move entity hider logic to plugin --- .../main/java/net/runelite/api/Client.java | 113 +------- .../net/runelite/api/hooks/Callbacks.java | 9 + .../net/runelite/client/callback/Hooks.java | 33 +++ .../entityhider/EntityHiderPlugin.java | 151 ++++++++--- .../entityhider/EntityHiderPluginTest.java | 242 ++++++++++++++++++ 5 files changed, 411 insertions(+), 137 deletions(-) create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/entityhider/EntityHiderPluginTest.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 3b0994fcd6..95785bea98 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -376,6 +376,13 @@ public interface Client extends OAuthApi, GameEngine */ Player getLocalPlayer(); + /** + * Get the local player's follower, such as a pet + * @return + */ + @Nullable + NPC getFollower(); + /** * Gets the item composition corresponding to an items ID. * @@ -1628,112 +1635,6 @@ public interface Client extends OAuthApi, GameEngine */ int getItemPressedDuration(); - /** - * Sets whether the client is hiding entities. - *

- * This method does not itself hide any entities. It behaves as a master - * switch for whether or not any of the related entities are hidden or - * shown. If this method is set to false, changing the configurations for - * specific entities will have no effect. - * - * @param state new entity hiding state - */ - void setIsHidingEntities(boolean state); - - /** - * Sets whether or not other players are hidden. - * - * @param state the new player hidden state - */ - void setOthersHidden(boolean state); - - /** - * Sets whether 2D sprites related to the other players are hidden. - * (ie. overhead prayers, PK skull) - * - * @param state the new player 2D hidden state - */ - void setOthersHidden2D(boolean state); - - /** - * Sets whether or not friends are hidden. - * - * @param state the new friends hidden state - */ - void setFriendsHidden(boolean state); - - /** - * Sets whether or not friends chat members are hidden. - * - * @param state the new friends chat member hidden state - */ - void setFriendsChatMembersHidden(boolean state); - - /** - * Sets whether or not clan members are hidden. - * - * @param state the new clan chat member hidden state - */ - void setClanChatMembersHidden(boolean state); - - /** - * Sets whether or not ignored players are hidden. - * - * @param state the new ignored player hidden state - */ - void setIgnoresHidden(boolean state); - - /** - * Sets whether the local player is hidden. - * - * @param state new local player hidden state - */ - void setLocalPlayerHidden(boolean state); - - /** - * Sets whether 2D sprites related to the local player are hidden. - * (ie. overhead prayers, PK skull) - * - * @param state new local player 2D hidden state - */ - void setLocalPlayerHidden2D(boolean state); - - /** - * Sets whether NPCs are hidden. - * - * @param state new NPC hidden state - */ - void setNPCsHidden(boolean state); - - /** - * Sets whether 2D sprites related to the NPCs are hidden. - * (ie. overhead prayers) - * - * @param state new NPC 2D hidden state - */ - void setNPCsHidden2D(boolean state); - - /** - * Sets whether Pets from other players are hidden. - * - * @param state new pet hidden state - */ - void setPetsHidden(boolean state); - - /** - * Sets whether attacking players or NPCs are hidden. - * - * @param state new attacker hidden state - */ - void setAttackersHidden(boolean state); - - /** - * Sets whether projectiles are hidden. - * - * @param state new projectile hidden state - */ - void setProjectilesHidden(boolean state); - /** * Gets an array of tile collision data. *

diff --git a/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java b/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java index 777f486e86..bf2c40b82b 100644 --- a/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java +++ b/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java @@ -30,6 +30,7 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.util.List; import net.runelite.api.MainBufferProvider; +import net.runelite.api.Renderable; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetItem; @@ -185,4 +186,12 @@ public interface Callbacks * @param keyEvent the key event */ void keyTyped(KeyEvent keyEvent); + + /** + * Called to test if a renderable should be drawn this frame + * @param renderable the renderable + * @param drawingUi if this is the 2d ui, such as hp bars or hitsplats + * @return false to prevent drawing + */ + boolean draw(Renderable renderable, boolean drawingUi); } diff --git a/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java b/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java index 65c877184b..a208da0fa8 100644 --- a/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java +++ b/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java @@ -36,6 +36,7 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.image.BufferedImage; import java.awt.image.VolatileImage; +import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; @@ -43,6 +44,7 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.MainBufferProvider; import net.runelite.api.RenderOverview; +import net.runelite.api.Renderable; import net.runelite.api.Skill; import net.runelite.api.WorldMapManager; import net.runelite.api.events.BeforeRender; @@ -108,6 +110,14 @@ public class Hooks implements Callbacks private static MainBufferProvider lastMainBufferProvider; private static Graphics2D lastGraphics; + @FunctionalInterface + public interface RenderableDrawListener + { + boolean draw(Renderable renderable, boolean ui); + } + + private final List renderableDrawListeners = new ArrayList<>(); + /** * Get the Graphics2D for the MainBufferProvider image * This caches the Graphics2D instance so it can be reused @@ -544,4 +554,27 @@ public class Hooks implements Callbacks ); eventBus.post(fakeXpDrop); } + + public void registerRenderableDrawListener(RenderableDrawListener listener) + { + renderableDrawListeners.add(listener); + } + + public void unregisterRenderableDrawListener(RenderableDrawListener listener) + { + renderableDrawListeners.remove(listener); + } + + @Override + public boolean draw(Renderable renderable, boolean drawingUi) + { + for (RenderableDrawListener renderableDrawListener : renderableDrawListeners) + { + if (!renderableDrawListener.draw(renderable, drawingUi)) + { + return false; + } + } + return true; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderPlugin.java index 2d34d5e813..a03d163fec 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderPlugin.java @@ -25,9 +25,16 @@ */ package net.runelite.client.plugins.entityhider; +import com.google.common.annotations.VisibleForTesting; import com.google.inject.Provides; import javax.inject.Inject; import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.Projectile; +import net.runelite.api.Renderable; +import net.runelite.api.Varbits; +import net.runelite.client.callback.Hooks; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; @@ -48,6 +55,25 @@ public class EntityHiderPlugin extends Plugin @Inject private EntityHiderConfig config; + @Inject + private Hooks hooks; + + private boolean hideOthers; + private boolean hideOthers2D; + private boolean hideFriends; + private boolean hideFriendsChatMembers; + private boolean hideClanMembers; + private boolean hideIgnoredPlayers; + private boolean hideLocalPlayer; + private boolean hideLocalPlayer2D; + private boolean hideNPCs; + private boolean hideNPCs2D; + private boolean hidePets; + private boolean hideAttackers; + private boolean hideProjectiles; + + private final Hooks.RenderableDrawListener drawListener = this::shouldDraw; + @Provides EntityHiderConfig provideConfig(ConfigManager configManager) { @@ -58,6 +84,14 @@ public class EntityHiderPlugin extends Plugin protected void startUp() { updateConfig(); + + hooks.registerRenderableDrawListener(drawListener); + } + + @Override + protected void shutDown() + { + hooks.unregisterRenderableDrawListener(drawListener); } @Subscribe @@ -71,52 +105,107 @@ public class EntityHiderPlugin extends Plugin private void updateConfig() { - client.setIsHidingEntities(true); + hideOthers = config.hideOthers(); + hideOthers2D = config.hideOthers2D(); - client.setOthersHidden(config.hideOthers()); - client.setOthersHidden2D(config.hideOthers2D()); + hideFriends = config.hideFriends(); + hideFriendsChatMembers = config.hideFriendsChatMembers(); + hideClanMembers = config.hideClanChatMembers(); + hideIgnoredPlayers = config.hideIgnores(); - client.setFriendsHidden(config.hideFriends()); - client.setFriendsChatMembersHidden(config.hideFriendsChatMembers()); - client.setClanChatMembersHidden(config.hideClanChatMembers()); - client.setIgnoresHidden(config.hideIgnores()); + hideLocalPlayer = config.hideLocalPlayer(); + hideLocalPlayer2D = config.hideLocalPlayer2D(); - client.setLocalPlayerHidden(config.hideLocalPlayer()); - client.setLocalPlayerHidden2D(config.hideLocalPlayer2D()); + hideNPCs = config.hideNPCs(); + hideNPCs2D = config.hideNPCs2D(); - client.setNPCsHidden(config.hideNPCs()); - client.setNPCsHidden2D(config.hideNPCs2D()); + hidePets = config.hidePets(); - client.setPetsHidden(config.hidePets()); + hideAttackers = config.hideAttackers(); - client.setAttackersHidden(config.hideAttackers()); - - client.setProjectilesHidden(config.hideProjectiles()); + hideProjectiles = config.hideProjectiles(); } - @Override - protected void shutDown() throws Exception + @VisibleForTesting + boolean shouldDraw(Renderable renderable, boolean drawingUI) { - client.setIsHidingEntities(false); + if (renderable instanceof Player) + { + Player player = (Player) renderable; + Player local = client.getLocalPlayer(); - client.setOthersHidden(false); - client.setOthersHidden2D(false); + if (player.getName() == null) + { + // player.isFriend() and player.isFriendsChatMember() npe when the player has a null name + return true; + } - client.setFriendsHidden(false); - client.setFriendsChatMembersHidden(false); - client.setClanChatMembersHidden(false); - client.setIgnoresHidden(false); + // Allow hiding local self in pvp, which is an established meta. + // It is more advantageous than renderself due to being able to still render local player 2d + if (player == local) + { + return !(drawingUI ? hideLocalPlayer2D : hideLocalPlayer); + } - client.setLocalPlayerHidden(false); - client.setLocalPlayerHidden2D(false); + final boolean inPvp = client.getVarbitValue(Varbits.PVP_SPEC_ORB) == 1; + if (inPvp) + { + // In PVP we only allow hiding everyone or no one + return !(drawingUI ? hideOthers2D : hideOthers); + } - client.setNPCsHidden(false); - client.setNPCsHidden2D(false); + if (hideAttackers && player.getInteracting() == local) + { + return false; // hide + } - client.setPetsHidden(false); + if (player.isFriend()) + { + return !hideFriends; + } + if (player.isFriendsChatMember()) + { + return !hideFriendsChatMembers; + } + if (player.isClanMember()) + { + return !hideClanMembers; + } + if (client.getIgnoreContainer().findByName(player.getName()) != null) + { + return !hideIgnoredPlayers; + } - client.setAttackersHidden(false); + return !(drawingUI ? hideOthers2D : hideOthers); + } + else if (renderable instanceof NPC) + { + NPC npc = (NPC) renderable; - client.setProjectilesHidden(false); + if (npc.getComposition().isFollower() && npc == client.getFollower()) + { + return !hidePets; + } + + if (npc.getInteracting() == client.getLocalPlayer()) + { + boolean b = hideAttackers; + // Kludge to make hide attackers only affect 2d or 3d if the 2d or 3d hide is on + // This allows hiding 2d for all npcs, including attackers. + if (hideNPCs2D || hideNPCs) + { + b &= drawingUI ? hideNPCs2D : hideNPCs; + } + return !b; + } + + return !(drawingUI ? hideNPCs2D : hideNPCs); + } + else if (renderable instanceof Projectile) + { + return !hideProjectiles; + } + + return true; } } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/entityhider/EntityHiderPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/entityhider/EntityHiderPluginTest.java new file mode 100644 index 0000000000..834a296498 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/entityhider/EntityHiderPluginTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2022, 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.entityhider; + +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Ignore; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.NameableContainer; +import net.runelite.api.Player; +import net.runelite.client.callback.Hooks; +import net.runelite.client.events.ConfigChanged; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EntityHiderPluginTest +{ + @Inject + EntityHiderPlugin plugin; + + @Mock + @Bind + Client client; + + @Mock + @Bind + EntityHiderConfig config; + + @Mock + @Bind + Hooks hooks; + + @Mock + NameableContainer ignoreNameableContainer; + + @Before + public void before() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + + when(client.getIgnoreContainer()).thenReturn(ignoreNameableContainer); + } + + @Test + public void testHideFriendsPositive() + { + when(config.hideOthers()).thenReturn(true); + when(config.hideFriends()).thenReturn(true); + + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup(EntityHiderConfig.GROUP); + plugin.onConfigChanged(configChanged); + + Player player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriend()).thenReturn(true); + + assertFalse(plugin.shouldDraw(player, false)); + + player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriend()).thenReturn(false); + + assertFalse(plugin.shouldDraw(player, false)); + } + + @Test + public void testHideFriendsNegative() + { + when(config.hideOthers()).thenReturn(true); + + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup(EntityHiderConfig.GROUP); + plugin.onConfigChanged(configChanged); + + Player player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriend()).thenReturn(false); + + assertFalse(plugin.shouldDraw(player, false)); + + player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriend()).thenReturn(true); + + assertTrue(plugin.shouldDraw(player, false)); + } + + @Test + public void testHideClansPositivie() + { + when(config.hideOthers()).thenReturn(true); + when(config.hideFriendsChatMembers()).thenReturn(true); + + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup(EntityHiderConfig.GROUP); + plugin.onConfigChanged(configChanged); + + Player player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriendsChatMember()).thenReturn(true); + + assertFalse(plugin.shouldDraw(player, false)); + + player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriendsChatMember()).thenReturn(false); + + assertFalse(plugin.shouldDraw(player, false)); + } + + @Test + public void testHideClansNegative() + { + when(config.hideOthers()).thenReturn(true); + + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup(EntityHiderConfig.GROUP); + plugin.onConfigChanged(configChanged); + + Player player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriendsChatMember()).thenReturn(false); + + assertFalse(plugin.shouldDraw(player, false)); + + player = mock(Player.class); + when(player.getName()).thenReturn("Adam"); + when(player.isFriendsChatMember()).thenReturn(true); + + assertTrue(plugin.shouldDraw(player, false)); + } + + // hidenpc hideattacker hidden? + // t t t iif attacker would be hidden + // t f f + // f t t + // f f f + @Test + public void testHideAndAttacker() + { + when(config.hideNPCs2D()).thenReturn(true); + when(config.hideAttackers()).thenReturn(true); + + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup(EntityHiderConfig.GROUP); + plugin.onConfigChanged(configChanged); + + NPCComposition composition = mock(NPCComposition.class); + + NPC npc = mock(NPC.class); + when(npc.getComposition()).thenReturn(composition); + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + when(npc.getInteracting()).thenReturn(player); + + assertFalse(plugin.shouldDraw(npc, true)); + assertTrue(plugin.shouldDraw(npc, false)); + } + + @Test + public void testHideAndNoAttacker() + { + when(config.hideNPCs2D()).thenReturn(true); + + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup(EntityHiderConfig.GROUP); + plugin.onConfigChanged(configChanged); + + NPCComposition composition = mock(NPCComposition.class); + + NPC npc = mock(NPC.class); + when(npc.getComposition()).thenReturn(composition); + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + when(npc.getInteracting()).thenReturn(player); + + assertTrue(plugin.shouldDraw(npc, true)); + assertTrue(plugin.shouldDraw(npc, false)); + } + + @Test + public void testHideAttacker() + { + when(config.hideAttackers()).thenReturn(true); + + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup(EntityHiderConfig.GROUP); + plugin.onConfigChanged(configChanged); + + NPCComposition composition = mock(NPCComposition.class); + + NPC npc = mock(NPC.class); + when(npc.getComposition()).thenReturn(composition); + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + when(npc.getInteracting()).thenReturn(player); + + assertFalse(plugin.shouldDraw(npc, true)); + assertFalse(plugin.shouldDraw(npc, false)); + } +} \ No newline at end of file