From 4d5857214eaf433f5264a3d38b4e6039e0f87b11 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 9 Feb 2020 16:06:40 -0500 Subject: [PATCH] loot tracker: add pickpocket events Co-authored-by: Daniel Cimento --- .../http/api/loottracker/LootRecordType.java | 1 + .../loottracker/LootTrackerService.java | 2 +- .../loottracker/LootTrackerPlugin.java | 106 ++++++++++++++---- .../loottracker/LootTrackerPluginTest.java | 94 ++++++++++++++++ 4 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java diff --git a/http-api/src/main/java/net/runelite/http/api/loottracker/LootRecordType.java b/http-api/src/main/java/net/runelite/http/api/loottracker/LootRecordType.java index c91817b410..33e622de45 100644 --- a/http-api/src/main/java/net/runelite/http/api/loottracker/LootRecordType.java +++ b/http-api/src/main/java/net/runelite/http/api/loottracker/LootRecordType.java @@ -29,5 +29,6 @@ public enum LootRecordType NPC, PLAYER, EVENT, + PICKPOCKET, UNKNOWN } diff --git a/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerService.java b/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerService.java index f7f9ac5004..ea2f5176a7 100644 --- a/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerService.java +++ b/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerService.java @@ -47,7 +47,7 @@ public class LootTrackerService " `first_time` timestamp NOT NULL DEFAULT current_timestamp(),\n" + " `last_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),\n" + " `accountId` int(11) NOT NULL,\n" + - " `type` enum('NPC','PLAYER','EVENT','UNKNOWN') NOT NULL,\n" + + " `type` enum('NPC','PLAYER','EVENT','PICKPOCKET','UNKNOWN') NOT NULL,\n" + " `eventId` varchar(255) NOT NULL,\n" + " `amount` int(11) NOT NULL,\n" + " PRIMARY KEY (`id`),\n" + diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java index 0864776476..44619a56b2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java @@ -25,9 +25,12 @@ */ package net.runelite.client.plugins.loottracker; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; import com.google.common.collect.Multiset; import com.google.common.collect.Multisets; import com.google.inject.Provides; @@ -66,6 +69,7 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.ItemContainerChanged; +import net.runelite.api.events.MenuOptionClicked; import net.runelite.api.events.WidgetLoaded; import net.runelite.api.widgets.WidgetID; import net.runelite.client.account.AccountSession; @@ -95,6 +99,7 @@ import net.runelite.http.api.loottracker.LootRecord; import net.runelite.http.api.loottracker.LootRecordType; import net.runelite.http.api.loottracker.LootTrackerClient; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.text.WordUtils; @PluginDescriptor( name = "Loot Tracker", @@ -137,6 +142,18 @@ public class LootTrackerPlugin extends Plugin // Last man standing map regions private static final Set LAST_MAN_STANDING_REGIONS = ImmutableSet.of(13658, 13659, 13914, 13915, 13916); + private static final Pattern PICKPOCKET_REGEX = Pattern.compile("You pick (the )?(?.+)'s? pocket.*"); + + /* + * This map is used when a pickpocket target has a different name in the chat message than their in-game name. + * Note that if the two NPCs can be found in the same place, there is a chance of race conditions + * occurring when changing targets mid-pickpocket, in which case a different solution may need to be considered. + */ + private static final Multimap PICKPOCKET_DISAMBIGUATION_MAP = ImmutableMultimap.of( + "H.A.M. Member", "Man", + "H.A.M. Member", "Woman" + ); + @Inject private ClientToolbar clientToolbar; @@ -166,8 +183,12 @@ public class LootTrackerPlugin extends Plugin private LootTrackerPanel panel; private NavigationButton navButton; - private String eventType; + @VisibleForTesting + String eventType; + @VisibleForTesting + LootRecordType lootRecordType; private boolean chestLooted; + private String lastPickpocketTarget; private List ignoredItems = new ArrayList<>(); @@ -370,13 +391,14 @@ public class LootTrackerPlugin extends Plugin } @Subscribe - public void onWidgetLoaded(WidgetLoaded event) + public void onWidgetLoaded(WidgetLoaded widgetLoaded) { + final String event; final ItemContainer container; - switch (event.getGroupId()) + switch (widgetLoaded.getGroupId()) { case (WidgetID.BARROWS_REWARD_GROUP_ID): - eventType = "Barrows"; + event = "Barrows"; container = client.getItemContainer(InventoryID.BARROWS_REWARD); break; case (WidgetID.CHAMBERS_OF_XERIC_REWARD_GROUP_ID): @@ -384,7 +406,7 @@ public class LootTrackerPlugin extends Plugin { return; } - eventType = "Chambers of Xeric"; + event = "Chambers of Xeric"; container = client.getItemContainer(InventoryID.CHAMBERS_OF_XERIC_CHEST); chestLooted = true; break; @@ -398,21 +420,22 @@ public class LootTrackerPlugin extends Plugin { return; } - eventType = "Theatre of Blood"; + event = "Theatre of Blood"; container = client.getItemContainer(InventoryID.THEATRE_OF_BLOOD_CHEST); chestLooted = true; break; case (WidgetID.CLUE_SCROLL_REWARD_GROUP_ID): // event type should be set via ChatMessage for clue scrolls. // Clue Scrolls use same InventoryID as Barrows + event = eventType; container = client.getItemContainer(InventoryID.BARROWS_REWARD); break; case (WidgetID.KINGDOM_GROUP_ID): - eventType = "Kingdom of Miscellania"; + event = "Kingdom of Miscellania"; container = client.getItemContainer(InventoryID.KINGDOM_OF_MISCELLANIA); break; case (WidgetID.FISHING_TRAWLER_REWARD_GROUP_ID): - eventType = "Fishing Trawler"; + event = "Fishing Trawler"; container = client.getItemContainer(InventoryID.FISHING_TRAWLER_REWARD); break; default: @@ -432,11 +455,11 @@ public class LootTrackerPlugin extends Plugin if (items.isEmpty()) { - log.debug("No items to find for Event: {} | Container: {}", eventType, container); + log.debug("No items to find for Event: {} | Container: {}", event, container); return; } - addLoot(eventType, -1, LootRecordType.EVENT, items); + addLoot(event, -1, LootRecordType.EVENT, items); } @Subscribe @@ -458,6 +481,7 @@ public class LootTrackerPlugin extends Plugin } eventType = CHEST_EVENT_TYPES.get(regionID); + lootRecordType = LootRecordType.EVENT; takeInventorySnapshot(); return; @@ -466,6 +490,7 @@ public class LootTrackerPlugin extends Plugin if (message.equals(HERBIBOAR_LOOTED_MESSAGE)) { eventType = HERBIBOAR_EVENT; + lootRecordType = LootRecordType.EVENT; takeInventorySnapshot(); return; @@ -475,6 +500,7 @@ public class LootTrackerPlugin extends Plugin if (HESPORI_REGION == regionID && message.equals(HESPORI_LOOTED_MESSAGE)) { eventType = HESPORI_EVENT; + lootRecordType = LootRecordType.EVENT; takeInventorySnapshot(); return; } @@ -482,6 +508,29 @@ public class LootTrackerPlugin extends Plugin if (GAUNTLET_LOBBY_REGION == regionID && message.equals(GAUNTLET_LOOTED_MESSAGE)) { eventType = GAUNTLET_EVENT; + lootRecordType = LootRecordType.EVENT; + takeInventorySnapshot(); + return; + } + + final Matcher pickpocketMatcher = PICKPOCKET_REGEX.matcher(message); + if (pickpocketMatcher.matches()) + { + // Get the target's name as listed in the chat box + String pickpocketTarget = WordUtils.capitalize(pickpocketMatcher.group("target")); + + // Occasional edge case where the pickpocket message doesn't list the correct name of the NPC (e.g. H.A.M. Members) + if (PICKPOCKET_DISAMBIGUATION_MAP.get(lastPickpocketTarget).contains(pickpocketTarget)) + { + eventType = lastPickpocketTarget; + lootRecordType = LootRecordType.PICKPOCKET; + } + else + { + eventType = pickpocketTarget; + lootRecordType = LootRecordType.PICKPOCKET; + } + takeInventorySnapshot(); return; } @@ -495,21 +544,27 @@ public class LootTrackerPlugin extends Plugin { case "beginner": eventType = "Clue Scroll (Beginner)"; + lootRecordType = LootRecordType.EVENT; break; case "easy": eventType = "Clue Scroll (Easy)"; + lootRecordType = LootRecordType.EVENT; break; case "medium": eventType = "Clue Scroll (Medium)"; + lootRecordType = LootRecordType.EVENT; break; case "hard": eventType = "Clue Scroll (Hard)"; + lootRecordType = LootRecordType.EVENT; break; case "elite": eventType = "Clue Scroll (Elite)"; + lootRecordType = LootRecordType.EVENT; break; case "master": eventType = "Clue Scroll (Master)"; + lootRecordType = LootRecordType.EVENT; break; } } @@ -518,18 +573,31 @@ public class LootTrackerPlugin extends Plugin @Subscribe public void onItemContainerChanged(ItemContainerChanged event) { + if (event.getContainerId() != InventoryID.INVENTORY.getId()) + { + return; + } + if (CHEST_EVENT_TYPES.containsValue(eventType) || HERBIBOAR_EVENT.equals(eventType) || HESPORI_EVENT.equals(eventType) - || GAUNTLET_EVENT.equals(eventType)) + || GAUNTLET_EVENT.equals(eventType) + || lootRecordType == LootRecordType.PICKPOCKET) { - if (event.getItemContainer() != client.getItemContainer(InventoryID.INVENTORY)) - { - return; - } - - processChestLoot(eventType, event.getItemContainer()); + processInventoryLoot(eventType, lootRecordType, event.getItemContainer()); eventType = null; + lootRecordType = null; + } + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked event) + { + // There are some pickpocket targets who show up in the chat box with a different name (e.g. H.A.M. members -> man/woman) + // We use the value selected from the right-click menu as a fallback for the event lookup in those cases. + if (event.getMenuOption().equals("Pickpocket")) + { + lastPickpocketTarget = Text.removeTags(event.getMenuTarget()); } } @@ -578,7 +646,7 @@ public class LootTrackerPlugin extends Plugin } } - private void processChestLoot(String chestType, ItemContainer inventoryContainer) + private void processInventoryLoot(String event, LootRecordType lootRecordType, ItemContainer inventoryContainer) { if (inventorySnapshot != null) { @@ -592,7 +660,7 @@ public class LootTrackerPlugin extends Plugin .map(e -> new ItemStack(e.getElement(), e.getCount(), client.getLocalPlayer().getLocalLocation())) .collect(Collectors.toList()); - addLoot(chestType, -1, LootRecordType.EVENT, items); + addLoot(event, -1, lootRecordType, items); inventorySnapshot = null; } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java new file mode 100644 index 0000000000..29a690c694 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java @@ -0,0 +1,94 @@ +/* + * 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.loottracker; + +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import java.util.concurrent.ScheduledExecutorService; +import javax.inject.Inject; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.Player; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ChatMessage; +import net.runelite.client.game.SpriteManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.http.api.loottracker.LootRecordType; +import static org.junit.Assert.assertEquals; +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 LootTrackerPluginTest +{ + @Mock + @Bind + private ScheduledExecutorService scheduledExecutorService; + + @Mock + @Bind + private Client client; + + @Mock + @Bind + private SpriteManager spriteManager; + + @Mock + @Bind + private InfoBoxManager infoBoxManager; + + @Inject + private LootTrackerPlugin lootTrackerPlugin; + + @Mock + @Bind + private LootTrackerConfig lootTrackerConfig; + + @Before + public void setUp() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + } + + @Test + public void testPickPocket() + { + Player player = mock(Player.class); + when(player.getWorldLocation()).thenReturn(new WorldPoint(0, 0, 0)); + when(client.getLocalPlayer()).thenReturn(player); + + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You pick the hero's pocket.", "", 0); + lootTrackerPlugin.onChatMessage(chatMessage); + + assertEquals("Hero", lootTrackerPlugin.eventType); + assertEquals(LootRecordType.PICKPOCKET, lootTrackerPlugin.lootRecordType); + } +} \ No newline at end of file