From e9c252e2271c76aa39aa6418578ea5ce371e5da7 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 20 Aug 2021 15:45:23 -0400 Subject: [PATCH] chat channel: fix join/leave world hop detection The ChatPlayers do not implement equals or hashCode, making them unsuitable as a hashmap key. Instead just use a linked list and compare the members using the comparable interface. --- .../chatchannel/ChatChannelPlugin.java | 88 +++++++------ .../chatchannel/ChatChannelPluginTest.java | 123 ++++++++++++++++++ 2 files changed, 172 insertions(+), 39 deletions(-) create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/chatchannel/ChatChannelPluginTest.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatchannel/ChatChannelPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatchannel/ChatChannelPlugin.java index d58fa8ad27..e4584f60a5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatchannel/ChatChannelPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatchannel/ChatChannelPlugin.java @@ -26,6 +26,7 @@ */ package net.runelite.client.plugins.chatchannel; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.Lists; @@ -36,9 +37,9 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.Iterator; -import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.ListIterator; import javax.inject.Inject; import lombok.AllArgsConstructor; import net.runelite.api.ChatLineBuffer; @@ -101,7 +102,8 @@ public class ChatChannelPlugin extends Plugin { private static final int MAX_CHATS = 10; private static final String RECENT_TITLE = "Recent FCs"; - private static final int MESSAGE_DELAY = 10; + @VisibleForTesting + static final int MESSAGE_DELAY = 10; @Inject private Client client; @@ -129,7 +131,7 @@ public class ChatChannelPlugin extends Plugin * queue of temporary messages added to the client */ private final Deque joinMessages = new ArrayDeque<>(); - private final Map activityBuffer = new LinkedHashMap<>(); + private final List activityBuffer = new LinkedList<>(); private int joinedTick; private boolean kickConfirmed = false; @@ -284,31 +286,38 @@ public class ChatChannelPlugin extends Plugin private void queueJoin(ChatPlayer member, MemberActivity.ChatType chatType) { - // attempt to filter out world hopping joins - if (!activityBuffer.containsKey(member)) + for (ListIterator iter = activityBuffer.listIterator(); iter.hasNext(); ) { - MemberActivity joinActivity = new MemberActivity(ActivityType.JOINED, chatType, - member, client.getTickCount()); - activityBuffer.put(member, joinActivity); - } - else - { - activityBuffer.remove(member); + MemberActivity activity = iter.next(); + + if (activity.getChatType() == chatType && activity.getMember().compareTo(member) == 0) + { + iter.remove(); + return; + } } + + MemberActivity activity = new MemberActivity(ActivityType.JOINED, chatType, + member, client.getTickCount()); + activityBuffer.add(activity); } private void queueLeave(ChatPlayer member, MemberActivity.ChatType chatType) { - if (!activityBuffer.containsKey(member)) + for (ListIterator iter = activityBuffer.listIterator(); iter.hasNext(); ) { - MemberActivity leaveActivity = new MemberActivity(ActivityType.LEFT, chatType, - member, client.getTickCount()); - activityBuffer.put(member, leaveActivity); - } - else - { - activityBuffer.remove(member); + MemberActivity activity = iter.next(); + + if (activity.getChatType() == chatType && activity.getMember().compareTo(member) == 0) + { + iter.remove(); + return; + } } + + MemberActivity activity = new MemberActivity(ActivityType.LEFT, chatType, + member, client.getTickCount()); + activityBuffer.add(activity); } @Subscribe @@ -386,32 +395,33 @@ public class ChatChannelPlugin extends Plugin } } - private void addActivityMessages() + @VisibleForTesting + void addActivityMessages() { if (activityBuffer.isEmpty()) { return; } - Iterator activityIt = activityBuffer.values().iterator(); - - while (activityIt.hasNext()) + for (ListIterator iter = activityBuffer.listIterator(); iter.hasNext(); ) { - MemberActivity activity = activityIt.next(); - - if (activity.getTick() < client.getTickCount() - MESSAGE_DELAY) + MemberActivity activity = iter.next(); + if (activity.getTick() >= client.getTickCount() - MESSAGE_DELAY) { - activityIt.remove(); - switch (activity.getChatType()) - { - case FRIENDS_CHAT: - addActivityMessage((FriendsChatMember) activity.getMember(), activity.getActivityType()); - break; - case CLAN_CHAT: - case GUEST_CHAT: - addClanActivityMessage((ClanChannelMember) activity.getMember(), activity.getActivityType(), activity.getChatType()); - break; - } + // everything after this is older + return; + } + + iter.remove(); + switch (activity.getChatType()) + { + case FRIENDS_CHAT: + addActivityMessage((FriendsChatMember) activity.getMember(), activity.getActivityType()); + break; + case CLAN_CHAT: + case GUEST_CHAT: + addClanActivityMessage((ClanChannelMember) activity.getMember(), activity.getActivityType(), activity.getChatType()); + break; } } } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/chatchannel/ChatChannelPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/chatchannel/ChatChannelPluginTest.java new file mode 100644 index 0000000000..00f5307cb4 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/chatchannel/ChatChannelPluginTest.java @@ -0,0 +1,123 @@ +/* + * 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.chatchannel; + +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.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.MessageNode; +import net.runelite.api.clan.ClanChannel; +import net.runelite.api.clan.ClanChannelMember; +import net.runelite.api.clan.ClanRank; +import net.runelite.api.clan.ClanSettings; +import net.runelite.api.events.ClanMemberJoined; +import net.runelite.api.events.ClanMemberLeft; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.config.ChatColorConfig; +import net.runelite.client.game.ChatIconManager; +import net.runelite.client.game.chatbox.ChatboxPanelManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import org.mockito.Mock; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ChatChannelPluginTest +{ + @Inject + private ChatChannelPlugin chatChannelPlugin; + + @Mock + @Bind + private Client client; + + @Mock + @Bind + private ChatIconManager chatIconManager; + + @Mock + @Bind + private ChatChannelConfig config; + + @Mock + @Bind + private ClientThread clientThread; + + @Mock + @Bind + private ChatboxPanelManager chatboxPanelManager; + + @Mock + @Bind + private ChatColorConfig chatColorConfig; + + @Mock + @Bind + private ChatMessageManager chatMessageManager; + + @Before + public void before() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + } + + @Test + public void testJoinLeave() + { + ClanChannel channel = mock(ClanChannel.class); + ClanSettings settings = mock(ClanSettings.class); + + when(client.getClanChannel()).thenReturn(channel); + lenient().when(client.getClanSettings()).thenReturn(settings); + when(config.clanChatShowJoinLeave()).thenReturn(true); + lenient().when(client.addChatMessage(any(ChatMessageType.class), anyString(), anyString(), anyString())).thenAnswer(a -> mock(MessageNode.class)); + + ClanChannelMember member = mock(ClanChannelMember.class); + lenient().when(member.getRank()).thenReturn(ClanRank.OWNER); + chatChannelPlugin.onClanMemberLeft(new ClanMemberLeft(channel, member)); + + ClanChannelMember member2 = mock(ClanChannelMember.class); + lenient().when(member2.getRank()).thenReturn(ClanRank.OWNER); + chatChannelPlugin.onClanMemberJoined(new ClanMemberJoined(channel, member2)); + + lenient().when(client.getTickCount()).thenReturn(ChatChannelPlugin.MESSAGE_DELAY + 1); + chatChannelPlugin.addActivityMessages(); + + verify(client, never()).addChatMessage(any(ChatMessageType.class), anyString(), anyString(), anyString()); + verify(member).compareTo(member2); + } +} \ No newline at end of file