From 5c3e5bf87ca24f79d2dedf3019e9c686759c50aa Mon Sep 17 00:00:00 2001 From: iProdigy Date: Tue, 15 Oct 2024 02:59:30 -0700 Subject: [PATCH 1/9] feat: add rarity for npc pickpocketing --- .../dinkplugin/notifiers/LootNotifier.java | 6 + .../util/AbstractRarityService.java | 106 ++++++++++++++++++ .../java/dinkplugin/util/RarityService.java | 105 +---------------- .../java/dinkplugin/util/ThievingService.java | 34 ++++++ src/main/resources/thieving.json | 1 + 5 files changed, 150 insertions(+), 102 deletions(-) create mode 100644 src/main/java/dinkplugin/util/AbstractRarityService.java create mode 100644 src/main/java/dinkplugin/util/ThievingService.java create mode 100644 src/main/resources/thieving.json diff --git a/src/main/java/dinkplugin/notifiers/LootNotifier.java b/src/main/java/dinkplugin/notifiers/LootNotifier.java index db87142e..d99594e3 100644 --- a/src/main/java/dinkplugin/notifiers/LootNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LootNotifier.java @@ -14,6 +14,7 @@ import dinkplugin.util.ItemUtils; import dinkplugin.util.KillCountService; import dinkplugin.util.MathUtils; +import dinkplugin.util.ThievingService; import dinkplugin.util.RarityService; import dinkplugin.util.Utils; import dinkplugin.util.WorldUtils; @@ -53,6 +54,9 @@ public class LootNotifier extends BaseNotifier { @Inject private RarityService rarityService; + @Inject + private ThievingService thievingService; + private final Collection itemNameAllowlist = new CopyOnWriteArrayList<>(); private final Collection itemNameDenylist = new CopyOnWriteArrayList<>(); @@ -162,6 +166,8 @@ private void handleNotify(Collection items, String dropper, LootRecor OptionalDouble rarity; if (type == LootRecordType.NPC) { rarity = rarityService.getRarity(dropper, item.getId(), item.getQuantity()); + } else if (type == LootRecordType.PICKPOCKET) { + rarity = thievingService.getRarity(dropper, item.getId(), item.getQuantity()); } else { rarity = OptionalDouble.empty(); } diff --git a/src/main/java/dinkplugin/util/AbstractRarityService.java b/src/main/java/dinkplugin/util/AbstractRarityService.java new file mode 100644 index 00000000..f361df2d --- /dev/null +++ b/src/main/java/dinkplugin/util/AbstractRarityService.java @@ -0,0 +1,106 @@ +package dinkplugin.util; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ItemComposition; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemVariationMapping; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public abstract class AbstractRarityService { + + protected final Gson gson; + protected final ItemManager itemManager; + protected final Map> dropsBySourceName; + + AbstractRarityService(String resourceName, int expectedSize, Gson gson, ItemManager itemManager) { + this.gson = gson; + this.itemManager = itemManager; + this.dropsBySourceName = new HashMap<>(expectedSize); + + Map> raw; + try (InputStream is = getClass().getResourceAsStream(resourceName); + Reader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) { + raw = gson.fromJson(reader, new TypeToken>>() {}.getType()); + } catch (Exception e) { + log.error("Failed to read monster drop rates", e); + return; + } + + raw.forEach((sourceName, rawDrops) -> { + List drops = rawDrops.stream() + .map(RawDrop::transform) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + dropsBySourceName.put(sourceName, drops); + }); + } + + public OptionalDouble getRarity(String sourceName, int itemId, int quantity) { + ItemComposition composition = itemId >= 0 ? itemManager.getItemComposition(itemId) : null; + int canonical = composition != null && composition.getNote() != -1 ? composition.getLinkedNoteId() : itemId; + String itemName = composition != null ? composition.getMembersName() : ""; + Collection variants = new HashSet<>( + ItemVariationMapping.getVariations(ItemVariationMapping.map(canonical)) + ); + return dropsBySourceName.getOrDefault(sourceName, Collections.emptyList()) + .stream() + .filter(drop -> drop.getMinQuantity() <= quantity && quantity <= drop.getMaxQuantity()) + .filter(drop -> { + int id = drop.getItemId(); + if (id == itemId) return true; + return variants.contains(id) && itemName.equals(itemManager.getItemComposition(id).getMembersName()); + }) + .mapToDouble(RareDrop::getProbability) + .reduce(Double::sum); + } + + @Value + protected static class RareDrop { + int itemId; + int minQuantity; + int maxQuantity; + double probability; + } + + @Data + @Setter(AccessLevel.PRIVATE) + private static class RawDrop { + private @SerializedName("i") int itemId; + private @SerializedName("r") Integer rolls; + private @SerializedName("d") double denominator; + private @SerializedName("q") Integer quantity; + private @SerializedName("m") Integer quantMin; + private @SerializedName("n") Integer quantMax; + + Collection transform() { + int rounds = rolls != null ? rolls : 1; + int min = quantMin != null ? quantMin : quantity; + int max = quantMax != null ? quantMax : quantity; + double prob = 1 / denominator; + + if (rounds == 1) { + return List.of(new RareDrop(itemId, min, max, prob)); + } + List drops = new ArrayList<>(rounds); + for (int successCount = 1; successCount <= rounds; successCount++) { + double density = MathUtils.binomialProbability(prob, rounds, successCount); + drops.add(new RareDrop(itemId, min * successCount, max * successCount, density)); + } + return drops; + } + } +} diff --git a/src/main/java/dinkplugin/util/RarityService.java b/src/main/java/dinkplugin/util/RarityService.java index fb556db4..529280a6 100644 --- a/src/main/java/dinkplugin/util/RarityService.java +++ b/src/main/java/dinkplugin/util/RarityService.java @@ -1,114 +1,15 @@ package dinkplugin.util; import com.google.gson.Gson; -import com.google.gson.annotations.SerializedName; -import com.google.gson.reflect.TypeToken; -import lombok.AccessLevel; -import lombok.Data; -import lombok.Setter; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.ItemComposition; import net.runelite.client.game.ItemManager; -import net.runelite.client.game.ItemVariationMapping; import javax.inject.Inject; import javax.inject.Singleton; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.OptionalDouble; -import java.util.stream.Collectors; -@Slf4j @Singleton -public class RarityService { - private final Map> dropsByNpcName = new HashMap<>(1024); - private @Inject Gson gson; - private @Inject ItemManager itemManager; - +public class RarityService extends AbstractRarityService { @Inject - void init() { - Map> raw; - try (InputStream is = getClass().getResourceAsStream("/npc_drops.json"); - Reader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) { - raw = gson.fromJson(reader, - new TypeToken>>() {}.getType()); - } catch (Exception e) { - log.error("Failed to read monster drop rates", e); - return; - } - - raw.forEach((npcName, rawDrops) -> { - List drops = rawDrops.stream() - .map(RawDrop::transform) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - dropsByNpcName.put(npcName, drops); - }); - } - - public OptionalDouble getRarity(String npcName, int itemId, int quantity) { - ItemComposition composition = itemId >= 0 ? itemManager.getItemComposition(itemId) : null; - int canonical = composition != null && composition.getNote() != -1 ? composition.getLinkedNoteId() : itemId; - String itemName = composition != null ? composition.getMembersName() : ""; - Collection variants = new HashSet<>( - ItemVariationMapping.getVariations(ItemVariationMapping.map(canonical)) - ); - return dropsByNpcName.getOrDefault(npcName, Collections.emptyList()) - .stream() - .filter(drop -> drop.getMinQuantity() <= quantity && quantity <= drop.getMaxQuantity()) - .filter(drop -> { - int id = drop.getItemId(); - if (id == itemId) return true; - return variants.contains(id) && itemName.equals(itemManager.getItemComposition(id).getMembersName()); - }) - .mapToDouble(Drop::getProbability) - .reduce(Double::sum); - } - - @Value - private static class Drop { - int itemId; - int minQuantity; - int maxQuantity; - double probability; - } - - @Data - @Setter(AccessLevel.PRIVATE) - private static class RawDrop { - private @SerializedName("i") int itemId; - private @SerializedName("r") Integer rolls; - private @SerializedName("d") double denominator; - private @SerializedName("q") Integer quantity; - private @SerializedName("m") Integer quantMin; - private @SerializedName("n") Integer quantMax; - - Collection transform() { - int rounds = rolls != null ? rolls : 1; - int min = quantMin != null ? quantMin : quantity; - int max = quantMax != null ? quantMax : quantity; - double prob = 1 / denominator; - - if (rounds == 1) { - return List.of(new Drop(itemId, min, max, prob)); - } - List drops = new ArrayList<>(rounds); - for (int successCount = 1; successCount <= rounds; successCount++) { - double density = MathUtils.binomialProbability(prob, rounds, successCount); - drops.add(new Drop(itemId, min * successCount, max * successCount, density)); - } - return drops; - } + RarityService(Gson gson, ItemManager itemManager) { + super("/npc_drops.json", 1024, gson, itemManager); } } diff --git a/src/main/java/dinkplugin/util/ThievingService.java b/src/main/java/dinkplugin/util/ThievingService.java new file mode 100644 index 00000000..8246a45a --- /dev/null +++ b/src/main/java/dinkplugin/util/ThievingService.java @@ -0,0 +1,34 @@ +package dinkplugin.util; + +import com.google.gson.Gson; +import net.runelite.api.ItemID; +import net.runelite.client.game.ItemManager; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.OptionalDouble; + +@Singleton +public class ThievingService extends AbstractRarityService { + + @Inject + ThievingService(Gson gson, ItemManager itemManager) { + super("/thieving.json", 32, gson, itemManager); + } + + @Override + public OptionalDouble getRarity(String sourceName, int itemId, int quantity) { + if (itemId == ItemID.BLOOD_SHARD) { + // https://oldschool.runescape.wiki/w/Blood_shard#Item_sources + return OptionalDouble.of(1.0 / 5000); + } + + if (itemId == ItemID.ENHANCED_CRYSTAL_TELEPORT_SEED) { + // https://oldschool.runescape.wiki/w/Enhanced_crystal_teleport_seed#Item_sources + return OptionalDouble.of(1.0 / 1024); + } + + return super.getRarity(sourceName, itemId, quantity); + } + +} diff --git a/src/main/resources/thieving.json b/src/main/resources/thieving.json new file mode 100644 index 00000000..957aacfd --- /dev/null +++ b/src/main/resources/thieving.json @@ -0,0 +1 @@ +{"Guard":[{"i":2809,"d":128,"q":1},{"i":22879,"d":896,"q":2}],"Gnome":[{"i":2809,"d":150,"q":1}],"H.A.M. Member":[{"i":4298,"d":100,"q":1},{"i":4300,"d":100,"q":1},{"i":4302,"d":100,"q":1},{"i":4304,"d":100,"q":1},{"i":4306,"d":100,"q":1},{"i":4308,"d":100,"q":1},{"i":4310,"d":100,"q":1}],"Hero":[{"i":12157,"d":1400,"q":1}],"Master Farmer":[{"i":5295,"d":302,"q":1},{"i":5296,"d":443,"q":1},{"i":5298,"d":947,"q":1},{"i":5299,"d":1389,"q":1},{"i":5300,"d":2083,"q":1},{"i":5301,"d":2976,"q":1},{"i":5303,"d":6944,"q":1},{"i":5304,"d":10417,"q":1},{"i":22879,"d":260,"q":1}],"Paladin":[{"i":3560,"d":1000,"q":1}],"TzHaar-Hur":[{"i":1617,"d":195,"q":1}],"Wealthy citizen":[{"i":2711,"d":85,"q":1}]} From 05257130beb60b2c2cbd2694b576300131d43a8f Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Tue, 15 Oct 2024 03:15:59 -0700 Subject: [PATCH 2/9] docs: update field description --- docs/json-examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json-examples.md b/docs/json-examples.md index d34c5129..76f037f7 100644 --- a/docs/json-examples.md +++ b/docs/json-examples.md @@ -248,7 +248,7 @@ The possible values for `extra.category` correspond to the [`LootRecordType`](ht `killCount` is only specified for NPC/EVENT loot with the base RuneLite Loot Tracker plugin enabled. -`rarity` is currently only populated for NPC drops. This data is (imperfectly) scraped from the wiki, so it may not be 100% accurate. Also, we do not report a rarity if the NPC always drops the item on every kill. +`rarity` is currently only populated for NPC drops (and some pickpocket events). This data is (imperfectly) scraped from the wiki, so it may not be 100% accurate. Also, we do not report a rarity if the NPC always drops the item on every kill. The items are valued at GE prices (when possible) if the user has not disabled the `Use actively traded price` base RuneLite setting. Otherwise, the store price of the item is used. From 9bc8302afbff72817af26d3f6eb1fc95702e5fb6 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Tue, 15 Oct 2024 03:19:46 -0700 Subject: [PATCH 3/9] chore: update npc rarity tests --- src/test/java/dinkplugin/util/RarityServiceTest.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/test/java/dinkplugin/util/RarityServiceTest.java b/src/test/java/dinkplugin/util/RarityServiceTest.java index 69c17d7b..086628ca 100644 --- a/src/test/java/dinkplugin/util/RarityServiceTest.java +++ b/src/test/java/dinkplugin/util/RarityServiceTest.java @@ -10,9 +10,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.Spy; import java.util.OptionalDouble; @@ -30,13 +28,11 @@ class RarityServiceTest extends MockedTestBase { @Bind private final Gson gson = RuneLiteAPI.GSON; - @Mock @Bind - private ItemManager itemManager; + private final ItemManager itemManager = Mockito.mock(ItemManager.class); - @Spy @Bind - private RarityService service; + private final RarityService service = Mockito.spy(new RarityService(gson, itemManager)); @Override @BeforeEach From 6eba5745c110e98f044f2d1e4d6d58f4c0e76a86 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Tue, 15 Oct 2024 03:33:08 -0700 Subject: [PATCH 4/9] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f57e78..c790ac0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Unreleased +- Minor: Add rarity information on select pickpocketing drops. (#571) - Bugfix: Enforce value threshold for always-dropped loot when rarity threshold is 1 and require both value and rarity is true. (#560) - Dev: Optimize regex performance for looted items on the item denylist. (#565) From 98ac2f56762dd5d79269c98c18a0f406fd65f9b1 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 17 Oct 2024 02:15:56 -0700 Subject: [PATCH 5/9] chore: add mock tests --- .../util/AbstractRarityServiceTest.java | 63 +++++++++++++ .../dinkplugin/util/RarityServiceTest.java | 48 +--------- .../dinkplugin/util/ThievingServiceTest.java | 92 +++++++++++++++++++ 3 files changed, 158 insertions(+), 45 deletions(-) create mode 100644 src/test/java/dinkplugin/util/AbstractRarityServiceTest.java create mode 100644 src/test/java/dinkplugin/util/ThievingServiceTest.java diff --git a/src/test/java/dinkplugin/util/AbstractRarityServiceTest.java b/src/test/java/dinkplugin/util/AbstractRarityServiceTest.java new file mode 100644 index 00000000..a0f62eec --- /dev/null +++ b/src/test/java/dinkplugin/util/AbstractRarityServiceTest.java @@ -0,0 +1,63 @@ +package dinkplugin.util; + +import com.google.gson.Gson; +import com.google.inject.testing.fieldbinder.Bind; +import dinkplugin.MockedTestBase; +import net.runelite.api.ItemComposition; +import net.runelite.client.game.ItemManager; +import net.runelite.http.api.RuneLiteAPI; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; + +import java.util.OptionalDouble; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +abstract class AbstractRarityServiceTest extends MockedTestBase { + private static final double DELTA = MathUtils.EPSILON; + + @Bind + protected final Gson gson = RuneLiteAPI.GSON; + + @Bind + protected final ItemManager itemManager = Mockito.mock(ItemManager.class); + + protected abstract AbstractRarityService getService(); + + @Override + @BeforeEach + protected void setUp() { + super.setUp(); + + // default item mock + Mockito.doAnswer(invocation -> { + ItemComposition comp = mock(ItemComposition.class); + when(comp.getMembersName()).thenReturn("?"); + when(comp.getNote()).thenReturn(-1); + return comp; + }).when(itemManager).getItemComposition(anyInt()); + } + + protected void test(String npcName, int itemId, int quantity, double expectedProbability) { + OptionalDouble rarity = getService().getRarity(npcName, itemId, quantity); + assertTrue(rarity.isPresent()); + assertEquals(expectedProbability, rarity.getAsDouble(), DELTA); + } + + protected void mockItem(int id, String name, boolean noted) { + ItemComposition item = mock(ItemComposition.class); + when(item.getName()).thenReturn(name); + when(item.getMembersName()).thenReturn(name); + when(item.getNote()).thenReturn(noted ? 799 : -1); + when(item.getLinkedNoteId()).thenReturn(noted ? id - 1 : id + 1); + when(itemManager.getItemComposition(id)).thenReturn(item); + } + + protected void mockItem(int id, String name) { + this.mockItem(id, name, false); + } +} diff --git a/src/test/java/dinkplugin/util/RarityServiceTest.java b/src/test/java/dinkplugin/util/RarityServiceTest.java index 086628ca..06471a80 100644 --- a/src/test/java/dinkplugin/util/RarityServiceTest.java +++ b/src/test/java/dinkplugin/util/RarityServiceTest.java @@ -1,12 +1,8 @@ package dinkplugin.util; -import com.google.gson.Gson; import com.google.inject.testing.fieldbinder.Bind; -import dinkplugin.MockedTestBase; -import net.runelite.api.ItemComposition; +import lombok.Getter; import net.runelite.api.ItemID; -import net.runelite.client.game.ItemManager; -import net.runelite.http.api.RuneLiteAPI; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,24 +10,12 @@ import java.util.OptionalDouble; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -class RarityServiceTest extends MockedTestBase { - - private static final double DELTA = MathUtils.EPSILON; - - @Bind - private final Gson gson = RuneLiteAPI.GSON; - - @Bind - private final ItemManager itemManager = Mockito.mock(ItemManager.class); +class RarityServiceTest extends AbstractRarityServiceTest { @Bind + @Getter private final RarityService service = Mockito.spy(new RarityService(gson, itemManager)); @Override @@ -39,14 +23,6 @@ class RarityServiceTest extends MockedTestBase { protected void setUp() { super.setUp(); - // default item mock - Mockito.doAnswer(invocation -> { - ItemComposition comp = mock(ItemComposition.class); - when(comp.getMembersName()).thenReturn("?"); - when(comp.getNote()).thenReturn(-1); - return comp; - }).when(itemManager).getItemComposition(anyInt()); - // actual item mocks mockItem(ItemID.DRAGON_SPEAR, "Dragon spear"); mockItem(ItemID.DRAGON_AXE, "Dragon axe"); @@ -265,22 +241,4 @@ void testVariations() { test("Tribesman", ItemID.SUPERANTIPOISON3, 1, 1.0 / 138); } - private void test(String npcName, int itemId, int quantity, double expectedProbability) { - OptionalDouble rarity = service.getRarity(npcName, itemId, quantity); - assertTrue(rarity.isPresent()); - assertEquals(expectedProbability, rarity.getAsDouble(), DELTA); - } - - private void mockItem(int id, String name, boolean noted) { - ItemComposition item = mock(ItemComposition.class); - when(item.getName()).thenReturn(name); - when(item.getMembersName()).thenReturn(name); - when(item.getNote()).thenReturn(noted ? 799 : -1); - when(item.getLinkedNoteId()).thenReturn(noted ? id - 1 : id + 1); - when(itemManager.getItemComposition(id)).thenReturn(item); - } - - private void mockItem(int id, String name) { - this.mockItem(id, name, false); - } } diff --git a/src/test/java/dinkplugin/util/ThievingServiceTest.java b/src/test/java/dinkplugin/util/ThievingServiceTest.java new file mode 100644 index 00000000..5e22b305 --- /dev/null +++ b/src/test/java/dinkplugin/util/ThievingServiceTest.java @@ -0,0 +1,92 @@ +package dinkplugin.util; + +import com.google.inject.testing.fieldbinder.Bind; +import lombok.Getter; +import net.runelite.api.ItemID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class ThievingServiceTest extends AbstractRarityServiceTest { + + @Bind + @Getter + private final ThievingService service = Mockito.spy(new ThievingService(gson, itemManager)); + + @Override + @BeforeEach + protected void setUp() { + super.setUp(); + + // item mocks + mockItem(ItemID.BLOOD_SHARD, "Blood shard"); + mockItem(ItemID.ENHANCED_CRYSTAL_TELEPORT_SEED, "Enhanced crystal teleport seed"); + mockItem(ItemID.UNCUT_DIAMOND, "Uncut diamond"); + mockItem(ItemID.CLUE_SCROLL_EASY, "Clue scroll (easy)"); + mockItem(ItemID.CLUE_SCROLL_EASY_2711, "Clue scroll (easy)"); + mockItem(ItemID.CLUE_SCROLL_MEDIUM, "Clue scroll (medium)"); + mockItem(ItemID.CLUE_SCROLL_MEDIUM_2809, "Clue scroll (medium)"); + mockItem(ItemID.CLUE_SCROLL_HARD, "Clue scroll (hard)"); + mockItem(ItemID.CLUE_SCROLL_HARD_3560, "Clue scroll (hard)"); + mockItem(ItemID.CLUE_SCROLL_ELITE, "Clue scroll (elite)"); + mockItem(ItemID.CLUE_SCROLL_ELITE_12157, "Clue scroll (elite)"); + mockItem(ItemID.CLUE_SCROLL_ELITE_12075, "Clue scroll (elite)"); + mockItem(ItemID.HAM_CLOAK, "Ham cloak"); + mockItem(ItemID.HAM_BOOTS, "Ham boots"); + mockItem(ItemID.SNAPE_GRASS_SEED, "Snape grass seed"); + mockItem(ItemID.SNAPDRAGON_SEED, "Snapdragon seed"); + } + + @Test + void testFarmer() { + test("Master Farmer", ItemID.SNAPE_GRASS_SEED, 1, 1.0 / 260); + test("Master Farmer", ItemID.SNAPDRAGON_SEED, 1, 1.0 / 2083); + } + + @Test + void testHam() { + test("H.A.M. Member", ItemID.HAM_CLOAK, 1, 1.0 / 100); + test("H.A.M. Member", ItemID.HAM_BOOTS, 1, 1.0 / 100); + } + + @Test + void testCitizen() { + test("Wealthy citizen", ItemID.CLUE_SCROLL_EASY, 1, 1.0 / 85); + } + + @Test + void testPaladin() { + test("Paladin", ItemID.CLUE_SCROLL_HARD, 1, 1.0 / 1000); + } + + @Test + void testGnome() { + test("Gnome", ItemID.CLUE_SCROLL_MEDIUM, 1, 1.0 / 150); + } + + @Test + void testHero() { + test("Hero", ItemID.CLUE_SCROLL_ELITE, 1, 1.0 / 1400); + test("Hero", ItemID.CLUE_SCROLL_ELITE_12075, 1, 1.0 / 1400); + test("Hero", ItemID.CLUE_SCROLL_ELITE_12157, 1, 1.0 / 1400); + } + + @Test + void testVyre() { + test("Caninelle Draynar", ItemID.BLOOD_SHARD, 1, 1.0 / 5000); + test("Grigor Rasputin", ItemID.BLOOD_SHARD, 1, 1.0 / 5000); + test("Valentina Diaemus", ItemID.BLOOD_SHARD, 1, 1.0 / 5000); + } + + @Test + void testElf() { + test("Arvel", ItemID.ENHANCED_CRYSTAL_TELEPORT_SEED, 1, 1.0 / 1024); + test("Indis", ItemID.ENHANCED_CRYSTAL_TELEPORT_SEED, 1, 1.0 / 1024); + } + + @Test + void testHur() { + test("TzHaar-Hur", ItemID.UNCUT_DIAMOND, 1, 1.0 / 195); + } + +} From 77153850885cca2c8bef53ca205dea601beba921 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 17 Oct 2024 02:33:50 -0700 Subject: [PATCH 6/9] chore: update loot notifier test --- .../notifiers/LootNotifierTest.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java index 05ba8eee..184b1d27 100644 --- a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java +++ b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java @@ -45,6 +45,7 @@ class LootNotifierTest extends MockedNotifierTest { + private static final int SHARD_PRICE = 10_000_000; private static final int LARRAN_PRICE = 150_000; private static final int RUBY_PRICE = 900; private static final int OPAL_PRICE = 600; @@ -78,6 +79,7 @@ protected void setUp() { when(localPlayer.getWorldLocation()).thenReturn(location); // init item mocks + mockItem(ItemID.BLOOD_SHARD, SHARD_PRICE, "Blood shard"); mockItem(ItemID.LARRANS_KEY, LARRAN_PRICE, "Larran's key"); mockItem(ItemID.RUBY, RUBY_PRICE, "Ruby"); mockItem(ItemID.OPAL, OPAL_PRICE, "Opal"); @@ -352,28 +354,31 @@ void testIgnoreNpc() { @Test void testNotifyPickpocket() { + String name = "Remus Kaninus"; NPC npc = Mockito.mock(NPC.class); - when(npc.getName()).thenReturn(LOOTED_NAME); - when(npc.getId()).thenReturn(9999); + when(npc.getName()).thenReturn(name); + when(npc.getId()).thenReturn(NpcID.REMUS_KANINUS); mockWorldNpcs(npc); // fire event - LootReceived event = new LootReceived(LOOTED_NAME, 99, LootRecordType.PICKPOCKET, Collections.singletonList(new ItemStack(ItemID.RUBY, 1)), 1); + LootReceived event = new LootReceived(name, -1, LootRecordType.PICKPOCKET, Collections.singletonList(new ItemStack(ItemID.BLOOD_SHARD, 1)), 1); plugin.onLootReceived(event); // verify notification message + double rarity = 1.0 / 5000; + String price = QuantityFormatter.quantityToStackSize(SHARD_PRICE); verifyCreateMessage( PRIMARY_WEBHOOK_URL, false, NotificationBody.builder() .text( Template.builder() - .template(String.format("%s has looted: 1 x {{ruby}} (%d) from {{source}} for %d gp", PLAYER_NAME, RUBY_PRICE, RUBY_PRICE)) - .replacement("{{ruby}}", Replacements.ofWiki("Ruby")) - .replacement("{{source}}", Replacements.ofWiki(LOOTED_NAME)) + .template(String.format("%s has looted: 1 x {{shard}} (%s) from {{source}} for %s gp", PLAYER_NAME, price, price)) + .replacement("{{shard}}", Replacements.ofWiki("Blood shard")) + .replacement("{{source}}", Replacements.ofWiki(name)) .build() ) - .extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), LOOTED_NAME, LootRecordType.PICKPOCKET, 1, null, null, 9999)) + .extra(new LootNotificationData(Collections.singletonList(new RareItemStack(ItemID.BLOOD_SHARD, 1, SHARD_PRICE, "Blood shard", rarity)), name, LootRecordType.PICKPOCKET, 1, rarity, null, NpcID.REMUS_KANINUS)) .type(NotificationType.LOOT) .build() ); From 990c7a631e4820f91621402ae5e51e3a4094f00c Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 17 Oct 2024 11:17:09 -0700 Subject: [PATCH 7/9] refactor: reduce memory usage of drops map --- src/main/java/dinkplugin/util/AbstractRarityService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/dinkplugin/util/AbstractRarityService.java b/src/main/java/dinkplugin/util/AbstractRarityService.java index f361df2d..af38afdf 100644 --- a/src/main/java/dinkplugin/util/AbstractRarityService.java +++ b/src/main/java/dinkplugin/util/AbstractRarityService.java @@ -41,10 +41,11 @@ public abstract class AbstractRarityService { } raw.forEach((sourceName, rawDrops) -> { - List drops = rawDrops.stream() + ArrayList drops = rawDrops.stream() .map(RawDrop::transform) .flatMap(Collection::stream) - .collect(Collectors.toList()); + .collect(Collectors.toCollection(ArrayList::new)); + drops.trimToSize(); dropsBySourceName.put(sourceName, drops); }); } From 96b5f4807a0a91b395c6c9a033c5fbad2ab188fe Mon Sep 17 00:00:00 2001 From: iProdigy Date: Fri, 18 Oct 2024 12:37:43 -0700 Subject: [PATCH 8/9] chore: add version description --- src/main/java/dinkplugin/VersionManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/dinkplugin/VersionManager.java b/src/main/java/dinkplugin/VersionManager.java index 9c860687..3b11e4de 100644 --- a/src/main/java/dinkplugin/VersionManager.java +++ b/src/main/java/dinkplugin/VersionManager.java @@ -128,5 +128,6 @@ public static Version of(@NotNull String version) { register("1.9.0", "Notifications now report monster drop rarity"); register("1.10.0", "Chat messages that match custom patterns can trigger notifications"); register("1.10.1", "Level notifier now triggers at XP milestones with 5M as the default interval"); + register("1.10.12", "Rarity is now reported for notable pickpocket loot"); } } From 3ffbf2066ef186c7f84a3d29b3555abaac5a78c7 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Fri, 18 Oct 2024 13:04:11 -0700 Subject: [PATCH 9/9] chore: add negative tests --- src/test/java/dinkplugin/util/ThievingServiceTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/dinkplugin/util/ThievingServiceTest.java b/src/test/java/dinkplugin/util/ThievingServiceTest.java index 5e22b305..89576011 100644 --- a/src/test/java/dinkplugin/util/ThievingServiceTest.java +++ b/src/test/java/dinkplugin/util/ThievingServiceTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import static org.junit.jupiter.api.Assertions.assertFalse; + class ThievingServiceTest extends AbstractRarityServiceTest { @Bind @@ -41,17 +43,23 @@ protected void setUp() { void testFarmer() { test("Master Farmer", ItemID.SNAPE_GRASS_SEED, 1, 1.0 / 260); test("Master Farmer", ItemID.SNAPDRAGON_SEED, 1, 1.0 / 2083); + + assertFalse(service.getRarity("Farmer", ItemID.SNAPDRAGON_SEED, 1).isPresent()); } @Test void testHam() { test("H.A.M. Member", ItemID.HAM_CLOAK, 1, 1.0 / 100); test("H.A.M. Member", ItemID.HAM_BOOTS, 1, 1.0 / 100); + + assertFalse(service.getRarity("Thief", ItemID.HAM_BOOTS, 1).isPresent()); } @Test void testCitizen() { test("Wealthy citizen", ItemID.CLUE_SCROLL_EASY, 1, 1.0 / 85); + + assertFalse(service.getRarity("Wealthy citizen", ItemID.CLUE_SCROLL_HARD, 1).isPresent()); } @Test