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