Repository: pajlads/DinkPlugin (java)

Issue:
Loot from Unsired does not notify
I received an Abyssal Dagger from using an Unsired but it did not post the loot. Loot threshold was set to 1M. If possible, when receiving an item with value from an Unsired it should notify the channel. Currently, bludgeon pieces have their individual worth posted in a clan chat when a player receives one.

Pull request description:
fix: track unsired drops for loot notifier
Closes #86

Given this case is hard to test, it does not appear that runelite wants to add it themselves (but if they do, simply remove our widget handling)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9e9db6..55a9659 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 - Minor: Add level up setting to skip notifications below a specified level. (#97)
 - Minor: Retry failed webhook messages with exponential backoff. (#94)
 - Minor: Add warning logs for improper runescape settings that impact notifiers. (#92)
+- Bugfix: Track Unsired drops for loot notifier. This is still experimental for wider testing - please report any problems to our [issue tracker](https://github.com/pajlads/DinkPlugin/issues) (#89)
 - Bugfix: Reduce timeouts from uploading screenshots on slow connections. (#96)
 - Bugfix: Ensure boss name is included in boss slayer messages. (#88)
 - Dev: Clean up utility code package. (#108)
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index 62fc5c0..b10cb83 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -174,6 +174,7 @@ public class DinkPlugin extends Plugin {
         questNotifier.onWidgetLoaded(event);
         clueNotifier.onWidgetLoaded(event);
         speedrunNotifier.onWidgetLoaded(event);
+        lootNotifier.onWidgetLoaded(event);
     }
 
     public void addChatWarning(String message) {
diff --git a/src/main/java/dinkplugin/notifiers/LootNotifier.java b/src/main/java/dinkplugin/notifiers/LootNotifier.java
index eb9235b..eb0455c 100644
--- a/src/main/java/dinkplugin/notifiers/LootNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/LootNotifier.java
@@ -7,7 +7,12 @@ import dinkplugin.util.Utils;
 import dinkplugin.notifiers.data.LootNotificationData;
 import dinkplugin.notifiers.data.SerializedItemStack;
 import dinkplugin.util.WorldUtils;
+import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.ItemComposition;
+import net.runelite.api.events.WidgetLoaded;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetID;
+import net.runelite.api.widgets.WidgetInfo;
 import net.runelite.client.events.NpcLootReceived;
 import net.runelite.client.events.PlayerLootReceived;
 import net.runelite.client.game.ItemManager;
@@ -20,8 +25,10 @@ import org.apache.commons.lang3.StringUtils;
 import javax.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
+@Slf4j
 public class LootNotifier extends BaseNotifier {
 
     @Inject
@@ -59,6 +66,35 @@ public class LootNotifier extends BaseNotifier {
         }
     }
 
+    public void onWidgetLoaded(WidgetLoaded event) {
+        if (!isEnabled()) return;
+
+        // special case: runelite client & loot tracker do not handle unsired loot at the time of writing
+        if (event.getGroupId() == WidgetID.DIALOG_SPRITE_GROUP_ID) {
+            Widget textWidget = client.getWidget(WidgetInfo.DIALOG_SPRITE_TEXT);
+            if (textWidget != null && StringUtils.containsIgnoreCase(textWidget.getText(), "The Font consumes the Unsired")) {
+                Widget spriteWidget = firstWithItem(WidgetInfo.DIALOG_SPRITE, WidgetInfo.DIALOG_SPRITE_SPRITE, WidgetInfo.DIALOG_SPRITE_TEXT);
+                if (hasItem(spriteWidget)) {
+                    ItemStack item = new ItemStack(
+                        spriteWidget.getItemId(),
+                        Math.max(spriteWidget.getItemQuantity(), 1),
+                        client.getLocalPlayer().getLocalLocation()
+                    );
+                    this.handleNotify(Collections.singletonList(item), "The Font of Consumption");
+                } else {
+                    Widget widget = client.getWidget(WidgetInfo.DIALOG_SPRITE);
+                    log.warn(
+                        "Failed to locate widget with item for Unsired loot. Children: {} - Nested: {} - Sprite: {} - Model: {}",
+                        widget != null && widget.getDynamicChildren() != null ? widget.getDynamicChildren().length : -1,
+                        widget != null && widget.getNestedChildren() != null ? widget.getNestedChildren().length : -1,
+                        widget != null ? widget.getSpriteId() : -1,
+                        widget != null ? widget.getModelId() : -1
+                    );
+                }
+            }
+        }
+    }
+
     private void handleNotify(Collection<ItemStack> items, String dropper) {
         final int minValue = config.minLootValue();
         final boolean icons = config.lootIcons();
@@ -105,4 +141,20 @@ public class LootNotifier extends BaseNotifier {
             );
         }
     }
+
+    private Widget firstWithItem(WidgetInfo... widgets) {
+        for (WidgetInfo info : widgets) {
+            Widget widget = client.getWidget(info);
+            if (hasItem(widget)) {
+                log.debug("Obtained item from widget via {}", info);
+                return widget;
+            }
+        }
+        return null;
+    }
+
+    private static boolean hasItem(Widget widget) {
+        return widget != null && widget.getItemId() >= 0;
+    }
+
 }


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
index 4993768..0f52add 100644
--- a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
@@ -9,6 +9,10 @@ import net.runelite.api.ItemID;
 import net.runelite.api.NPC;
 import net.runelite.api.Player;
 import net.runelite.api.coords.WorldPoint;
+import net.runelite.api.events.WidgetLoaded;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetID;
+import net.runelite.api.widgets.WidgetInfo;
 import net.runelite.client.events.NpcLootReceived;
 import net.runelite.client.events.PlayerLootReceived;
 import net.runelite.client.game.ItemManager;
@@ -232,6 +236,38 @@ class LootNotifierTest extends MockedNotifierTest {
         );
     }
 
+    @Test
+    void testNotifyUnsired() {
+        // prepare mocks
+        Widget spriteWidget = mock(Widget.class);
+        when(spriteWidget.getItemId()).thenReturn(ItemID.ABYSSAL_WHIP);
+        when(client.getWidget(WidgetInfo.DIALOG_SPRITE)).thenReturn(spriteWidget);
+        final int whipPrice = 1_500_000;
+        mockItem(itemManager, ItemID.ABYSSAL_WHIP, whipPrice, "Abyssal Whip");
+
+        Widget textWidget = mock(Widget.class);
+        when(textWidget.getText()).thenReturn("The Font consumes the Unsired and returns you a reward.");
+        when(client.getWidget(WidgetInfo.DIALOG_SPRITE_TEXT)).thenReturn(textWidget);
+
+        // fire event
+        WidgetLoaded event = new WidgetLoaded();
+        event.setGroupId(WidgetID.DIALOG_SPRITE_GROUP_ID);
+        notifier.onWidgetLoaded(event);
+
+        // verify notification message
+        String source = "The Font of Consumption";
+        String formattedPrice = QuantityFormatter.quantityToStackSize(whipPrice);
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .content(String.format("%s has looted: %s from %s for %s gp", PLAYER_NAME, "1 x Abyssal Whip (" + formattedPrice + ")", source, formattedPrice))
+                .extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.ABYSSAL_WHIP, 1, whipPrice, "Abyssal Whip")), source))
+                .type(NotificationType.LOOT)
+                .build()
+        );
+    }
+
     @Test
     void testIgnoreRepeated() {
         // fire event