Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(loot): add config to require both rarity and value to notify #499

Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Unreleased

- Minor: Allow filtering of loot notifications if both rarity and value thresholds are not met. (#499)

## 1.10.2

- Minor: Allow RuneLite notifier messages to trigger the Dink chat notifier. (#493)
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/dinkplugin/DinkPluginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -951,14 +951,27 @@ default int lootRarityThreshold() {
return 0;
}

@ConfigItem(
keyName = "lootRarityValueIntersection",
name = "Require both Rarity and Value",
description = "Whether items must exceed <i>both</i> the Min Value AND Rarity thresholds to be notified.<br/>" +
"Does not apply to drops where Dink lacks rarity data.<br/>" +
"Currently only impacts NPC drops",
position = 39,
section = lootSection
)
default boolean lootRarityValueIntersection() {
return false;
}

@ConfigItem(
keyName = "lootNotifMessage",
name = "Notification Message",
description = "The message to be sent through the webhook.<br/>" +
"Use %USERNAME% to insert your username<br/>" +
"Use %LOOT% to insert the loot<br/>" +
"Use %SOURCE% to show the source of the loot",
position = 39,
position = 40,
section = lootSection
)
default String lootNotifyMessage() {
Expand Down
30 changes: 25 additions & 5 deletions src/main/java/dinkplugin/notifiers/LootNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -158,17 +159,17 @@ private void handleNotify(Collection<ItemStack> items, String dropper, LootRecor
long totalStackValue = 0;
boolean sendMessage = false;
SerializedItemStack max = null;
Collection<Integer> allowedIds = new HashSet<>(reduced.size());
Collection<Integer> deniedIds = new HashSet<>(reduced.size());
Map<Integer, SerializedItemStack> candidates = new LinkedHashMap<>(reduced.size() * 3 / 2 + 1); // maintaining insertion order simplifies testing

for (ItemStack item : reduced) {
SerializedItemStack stack = ItemUtils.stackFromItem(itemManager, item.getId(), item.getQuantity());
long totalPrice = stack.getTotalPrice();
boolean worthy = totalPrice >= minValue || matches(itemNameAllowlist, stack.getName());
boolean worthy = matches(itemNameAllowlist, stack.getName()) ? allowedIds.add(item.getId()) : totalPrice >= minValue;
if (!matches(itemNameDenylist, stack.getName())) {
if (worthy) {
sendMessage = true;
lootMessage.component(ItemUtils.templateStack(stack, true));
if (icons) embeds.add(Embed.ofImage(ItemUtils.getItemImageUrl(item.getId())));
candidates.put(item.getId(), stack);
}
if (max == null || totalPrice > max.getTotalPrice()) {
max = stack;
Expand All @@ -184,9 +185,28 @@ private void handleNotify(Collection<ItemStack> items, String dropper, LootRecor
Collection<SerializedItemStack> augmentedItems = rarityResult.getKey();
RareItemStack rarest = rarityResult.getValue().orElse(null);

if (config.lootRarityValueIntersection() && config.lootRarityThreshold() > 0) {
final double rarityThreshold = 1.0 / config.lootRarityThreshold();
for (SerializedItemStack augmented : augmentedItems) {
if (augmented instanceof RareItemStack) {
int id = augmented.getId();
double rarity = ((RareItemStack) augmented).getRarity();
if (DoubleMath.fuzzyCompare(rarity, rarityThreshold, MathUtils.EPSILON) > 0 && !allowedIds.contains(id)) {
candidates.remove(id);
}
}
}
}

sendMessage = !candidates.isEmpty();
for (SerializedItemStack stack : candidates.values()) {
lootMessage.component(ItemUtils.templateStack(stack, true));
if (icons) embeds.add(Embed.ofImage(ItemUtils.getItemImageUrl(stack.getId())));
}

Evaluable lootMsg;
if (!sendMessage) {
if (sufficientlyRare(rarest)) {
if (!config.lootRarityValueIntersection() && sufficientlyRare(rarest)) {
// allow notifications for rare drops, even if below configured min loot value
sendMessage = true;
lootMsg = Replacements.ofMultiple(" ",
Expand Down
77 changes: 77 additions & 0 deletions src/test/java/dinkplugin/notifiers/LootNotifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,83 @@ void testNotifyAmascutExpert() {
);
}

@Test
void testNotifyRarityValueIntersectionValue() {
// update config mocks
when(config.minLootValue()).thenReturn(LARRAN_PRICE);
when(config.lootRarityThreshold()).thenReturn(100);
when(config.lootRarityValueIntersection()).thenReturn(true);

// prepare mocks
NPC npc = mock(NPC.class);
String name = "Ice spider";
when(npc.getName()).thenReturn(name);

// fire event
double rarity = 1.0 / 208;
NpcLootReceived event = new NpcLootReceived(npc, List.of(new ItemStack(ItemID.LARRANS_KEY, 1)));
plugin.onNpcLootReceived(event);

// verify notification message
String value = QuantityFormatter.quantityToStackSize(LARRAN_PRICE);
verify(messageHandler).createMessage(
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.text(
Template.builder()
.template(String.format("%s has looted: 1 x {{key}} (%s) from {{source}} for %s gp", PLAYER_NAME, value, value))
.replacement("{{key}}", Replacements.ofWiki("Larran's key"))
.replacement("{{source}}", Replacements.ofWiki(name))
.build()
)
.extra(new LootNotificationData(List.of(new RareItemStack(ItemID.LARRANS_KEY, 1, LARRAN_PRICE, "Larran's key", rarity)), name, LootRecordType.NPC, 1, rarity))
.type(NotificationType.LOOT)
.thumbnailUrl(ItemUtils.getItemImageUrl(ItemID.LARRANS_KEY))
.build()
);
}

@Test
void testNotifyRarityValueIntersectionRarityTooLow() {
// update config mocks
when(config.minLootValue()).thenReturn(LARRAN_PRICE - 1);
when(config.lootRarityThreshold()).thenReturn(1000);
when(config.lootRarityValueIntersection()).thenReturn(true);

// prepare mocks
NPC npc = mock(NPC.class);
String name = "Ice spider";
when(npc.getName()).thenReturn(name);

// fire event
NpcLootReceived event = new NpcLootReceived(npc, List.of(new ItemStack(ItemID.LARRANS_KEY, 1)));
plugin.onNpcLootReceived(event);

// verify notification message doesn't fire
verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
}

@Test
void testNotifyRarityValueIntersectionValueTooLow() {
// update config mocks
when(config.minLootValue()).thenReturn(LARRAN_PRICE + 1);
when(config.lootRarityThreshold()).thenReturn(100);
when(config.lootRarityValueIntersection()).thenReturn(true);

// prepare mocks
NPC npc = mock(NPC.class);
String name = "Ice spider";
when(npc.getName()).thenReturn(name);

// fire event
NpcLootReceived event = new NpcLootReceived(npc, List.of(new ItemStack(ItemID.LARRANS_KEY, 1)));
plugin.onNpcLootReceived(event);

// verify notification message doesn't fire
verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
}

@Test
void testDisabled() {
// disable notifier
Expand Down
Loading