loot tracker: add pickpocket events

Co-authored-by: Daniel Cimento <daniel.cimento@mail.mcgill.ca>
This commit is contained in:
Adam
2020-02-09 16:06:40 -05:00
committed by Adam
parent acfbf24e07
commit 4d5857214e
4 changed files with 183 additions and 20 deletions

View File

@@ -29,5 +29,6 @@ public enum LootRecordType
NPC, NPC,
PLAYER, PLAYER,
EVENT, EVENT,
PICKPOCKET,
UNKNOWN UNKNOWN
} }

View File

@@ -47,7 +47,7 @@ public class LootTrackerService
" `first_time` timestamp NOT NULL DEFAULT current_timestamp(),\n" + " `first_time` timestamp NOT NULL DEFAULT current_timestamp(),\n" +
" `last_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),\n" + " `last_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),\n" +
" `accountId` int(11) NOT NULL,\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" + " `eventId` varchar(255) NOT NULL,\n" +
" `amount` int(11) NOT NULL,\n" + " `amount` int(11) NOT NULL,\n" +
" PRIMARY KEY (`id`),\n" + " PRIMARY KEY (`id`),\n" +

View File

@@ -25,9 +25,12 @@
*/ */
package net.runelite.client.plugins.loottracker; package net.runelite.client.plugins.loottracker;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultiset; import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset; import com.google.common.collect.Multiset;
import com.google.common.collect.Multisets; import com.google.common.collect.Multisets;
import com.google.inject.Provides; 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.ChatMessage;
import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.ItemContainerChanged; import net.runelite.api.events.ItemContainerChanged;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.events.WidgetLoaded; import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.WidgetID; import net.runelite.api.widgets.WidgetID;
import net.runelite.client.account.AccountSession; 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.LootRecordType;
import net.runelite.http.api.loottracker.LootTrackerClient; import net.runelite.http.api.loottracker.LootTrackerClient;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.text.WordUtils;
@PluginDescriptor( @PluginDescriptor(
name = "Loot Tracker", name = "Loot Tracker",
@@ -137,6 +142,18 @@ public class LootTrackerPlugin extends Plugin
// Last man standing map regions // Last man standing map regions
private static final Set<Integer> LAST_MAN_STANDING_REGIONS = ImmutableSet.of(13658, 13659, 13914, 13915, 13916); private static final Set<Integer> LAST_MAN_STANDING_REGIONS = ImmutableSet.of(13658, 13659, 13914, 13915, 13916);
private static final Pattern PICKPOCKET_REGEX = Pattern.compile("You pick (the )?(?<target>.+)'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<String, String> PICKPOCKET_DISAMBIGUATION_MAP = ImmutableMultimap.of(
"H.A.M. Member", "Man",
"H.A.M. Member", "Woman"
);
@Inject @Inject
private ClientToolbar clientToolbar; private ClientToolbar clientToolbar;
@@ -166,8 +183,12 @@ public class LootTrackerPlugin extends Plugin
private LootTrackerPanel panel; private LootTrackerPanel panel;
private NavigationButton navButton; private NavigationButton navButton;
private String eventType; @VisibleForTesting
String eventType;
@VisibleForTesting
LootRecordType lootRecordType;
private boolean chestLooted; private boolean chestLooted;
private String lastPickpocketTarget;
private List<String> ignoredItems = new ArrayList<>(); private List<String> ignoredItems = new ArrayList<>();
@@ -370,13 +391,14 @@ public class LootTrackerPlugin extends Plugin
} }
@Subscribe @Subscribe
public void onWidgetLoaded(WidgetLoaded event) public void onWidgetLoaded(WidgetLoaded widgetLoaded)
{ {
final String event;
final ItemContainer container; final ItemContainer container;
switch (event.getGroupId()) switch (widgetLoaded.getGroupId())
{ {
case (WidgetID.BARROWS_REWARD_GROUP_ID): case (WidgetID.BARROWS_REWARD_GROUP_ID):
eventType = "Barrows"; event = "Barrows";
container = client.getItemContainer(InventoryID.BARROWS_REWARD); container = client.getItemContainer(InventoryID.BARROWS_REWARD);
break; break;
case (WidgetID.CHAMBERS_OF_XERIC_REWARD_GROUP_ID): case (WidgetID.CHAMBERS_OF_XERIC_REWARD_GROUP_ID):
@@ -384,7 +406,7 @@ public class LootTrackerPlugin extends Plugin
{ {
return; return;
} }
eventType = "Chambers of Xeric"; event = "Chambers of Xeric";
container = client.getItemContainer(InventoryID.CHAMBERS_OF_XERIC_CHEST); container = client.getItemContainer(InventoryID.CHAMBERS_OF_XERIC_CHEST);
chestLooted = true; chestLooted = true;
break; break;
@@ -398,21 +420,22 @@ public class LootTrackerPlugin extends Plugin
{ {
return; return;
} }
eventType = "Theatre of Blood"; event = "Theatre of Blood";
container = client.getItemContainer(InventoryID.THEATRE_OF_BLOOD_CHEST); container = client.getItemContainer(InventoryID.THEATRE_OF_BLOOD_CHEST);
chestLooted = true; chestLooted = true;
break; break;
case (WidgetID.CLUE_SCROLL_REWARD_GROUP_ID): case (WidgetID.CLUE_SCROLL_REWARD_GROUP_ID):
// event type should be set via ChatMessage for clue scrolls. // event type should be set via ChatMessage for clue scrolls.
// Clue Scrolls use same InventoryID as Barrows // Clue Scrolls use same InventoryID as Barrows
event = eventType;
container = client.getItemContainer(InventoryID.BARROWS_REWARD); container = client.getItemContainer(InventoryID.BARROWS_REWARD);
break; break;
case (WidgetID.KINGDOM_GROUP_ID): case (WidgetID.KINGDOM_GROUP_ID):
eventType = "Kingdom of Miscellania"; event = "Kingdom of Miscellania";
container = client.getItemContainer(InventoryID.KINGDOM_OF_MISCELLANIA); container = client.getItemContainer(InventoryID.KINGDOM_OF_MISCELLANIA);
break; break;
case (WidgetID.FISHING_TRAWLER_REWARD_GROUP_ID): case (WidgetID.FISHING_TRAWLER_REWARD_GROUP_ID):
eventType = "Fishing Trawler"; event = "Fishing Trawler";
container = client.getItemContainer(InventoryID.FISHING_TRAWLER_REWARD); container = client.getItemContainer(InventoryID.FISHING_TRAWLER_REWARD);
break; break;
default: default:
@@ -432,11 +455,11 @@ public class LootTrackerPlugin extends Plugin
if (items.isEmpty()) 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; return;
} }
addLoot(eventType, -1, LootRecordType.EVENT, items); addLoot(event, -1, LootRecordType.EVENT, items);
} }
@Subscribe @Subscribe
@@ -458,6 +481,7 @@ public class LootTrackerPlugin extends Plugin
} }
eventType = CHEST_EVENT_TYPES.get(regionID); eventType = CHEST_EVENT_TYPES.get(regionID);
lootRecordType = LootRecordType.EVENT;
takeInventorySnapshot(); takeInventorySnapshot();
return; return;
@@ -466,6 +490,7 @@ public class LootTrackerPlugin extends Plugin
if (message.equals(HERBIBOAR_LOOTED_MESSAGE)) if (message.equals(HERBIBOAR_LOOTED_MESSAGE))
{ {
eventType = HERBIBOAR_EVENT; eventType = HERBIBOAR_EVENT;
lootRecordType = LootRecordType.EVENT;
takeInventorySnapshot(); takeInventorySnapshot();
return; return;
@@ -475,6 +500,7 @@ public class LootTrackerPlugin extends Plugin
if (HESPORI_REGION == regionID && message.equals(HESPORI_LOOTED_MESSAGE)) if (HESPORI_REGION == regionID && message.equals(HESPORI_LOOTED_MESSAGE))
{ {
eventType = HESPORI_EVENT; eventType = HESPORI_EVENT;
lootRecordType = LootRecordType.EVENT;
takeInventorySnapshot(); takeInventorySnapshot();
return; return;
} }
@@ -482,6 +508,29 @@ public class LootTrackerPlugin extends Plugin
if (GAUNTLET_LOBBY_REGION == regionID && message.equals(GAUNTLET_LOOTED_MESSAGE)) if (GAUNTLET_LOBBY_REGION == regionID && message.equals(GAUNTLET_LOOTED_MESSAGE))
{ {
eventType = GAUNTLET_EVENT; 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(); takeInventorySnapshot();
return; return;
} }
@@ -495,21 +544,27 @@ public class LootTrackerPlugin extends Plugin
{ {
case "beginner": case "beginner":
eventType = "Clue Scroll (Beginner)"; eventType = "Clue Scroll (Beginner)";
lootRecordType = LootRecordType.EVENT;
break; break;
case "easy": case "easy":
eventType = "Clue Scroll (Easy)"; eventType = "Clue Scroll (Easy)";
lootRecordType = LootRecordType.EVENT;
break; break;
case "medium": case "medium":
eventType = "Clue Scroll (Medium)"; eventType = "Clue Scroll (Medium)";
lootRecordType = LootRecordType.EVENT;
break; break;
case "hard": case "hard":
eventType = "Clue Scroll (Hard)"; eventType = "Clue Scroll (Hard)";
lootRecordType = LootRecordType.EVENT;
break; break;
case "elite": case "elite":
eventType = "Clue Scroll (Elite)"; eventType = "Clue Scroll (Elite)";
lootRecordType = LootRecordType.EVENT;
break; break;
case "master": case "master":
eventType = "Clue Scroll (Master)"; eventType = "Clue Scroll (Master)";
lootRecordType = LootRecordType.EVENT;
break; break;
} }
} }
@@ -518,18 +573,31 @@ public class LootTrackerPlugin extends Plugin
@Subscribe @Subscribe
public void onItemContainerChanged(ItemContainerChanged event) public void onItemContainerChanged(ItemContainerChanged event)
{ {
if (event.getContainerId() != InventoryID.INVENTORY.getId())
{
return;
}
if (CHEST_EVENT_TYPES.containsValue(eventType) if (CHEST_EVENT_TYPES.containsValue(eventType)
|| HERBIBOAR_EVENT.equals(eventType) || HERBIBOAR_EVENT.equals(eventType)
|| HESPORI_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)) processInventoryLoot(eventType, lootRecordType, event.getItemContainer());
{
return;
}
processChestLoot(eventType, event.getItemContainer());
eventType = null; 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) if (inventorySnapshot != null)
{ {
@@ -592,7 +660,7 @@ public class LootTrackerPlugin extends Plugin
.map(e -> new ItemStack(e.getElement(), e.getCount(), client.getLocalPlayer().getLocalLocation())) .map(e -> new ItemStack(e.getElement(), e.getCount(), client.getLocalPlayer().getLocalLocation()))
.collect(Collectors.toList()); .collect(Collectors.toList());
addLoot(chestType, -1, LootRecordType.EVENT, items); addLoot(event, -1, lootRecordType, items);
inventorySnapshot = null; inventorySnapshot = null;
} }

View File

@@ -0,0 +1,94 @@
/*
* Copyright (c) 2020, Adam <Adam@sigterm.info>
* 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);
}
}