Repository: pajlads/DinkPlugin (java)

Issue:
Corrupted Gauntlet Reported as Regular Gauntlet
### Checklist

- [X] I'm reporting a problem with Dink
- [X] I've searched the [issues and pull requests](https://github.com/pajlads/Dinkplugin/issues?q=) for similar looking reports

### Describe your issue

As the title says, upon completion of a Corrupted Gauntlet, Dink sees the chest loot as the regular Gauntlet and displays an incorrect KC.


### Screenshots

![NVIDIA_Share_MbGBr6IHjN](https://github.com/pajlads/DinkPlugin/assets/21299867/253a6b60-c127-4e93-b1d8-24a866a110f5)
![Discord_ULK2xqzAKi](https://github.com/pajlads/DinkPlugin/assets/21299867/4aa46499-a346-4eb7-bc6d-4c8684ceb0fa)


### Runelite Logs

N/A

### Runelite Version

1.10.28.1

Pull request description:
fix(loot): distinguish corrupted from normal gauntlet
Closes #469 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de8937f..388de6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## Unreleased
 
 - Minor: Fire notifications for XP milestones beyond level 99. (#462)
+- Bugfix: Distinguish corrupted from normal Gauntlet for loot notifications. (#470)
 - Bugfix: Avoid double PK notification when multi-tick special attack had already killed the target. (#467)
 - Dev: Avoid benign NPE when config items are reset. (#464)
 
diff --git a/src/main/java/dinkplugin/notifiers/KillCountNotifier.java b/src/main/java/dinkplugin/notifiers/KillCountNotifier.java
index 3e036ad..992732b 100644
--- a/src/main/java/dinkplugin/notifiers/KillCountNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/KillCountNotifier.java
@@ -4,6 +4,7 @@ import dinkplugin.message.Embed;
 import dinkplugin.message.templating.Replacements;
 import dinkplugin.message.templating.Template;
 import dinkplugin.util.ItemUtils;
+import dinkplugin.util.KillCountService;
 import dinkplugin.util.TimeUtils;
 import dinkplugin.util.Utils;
 import dinkplugin.message.NotificationBody;
@@ -257,10 +258,10 @@ public class KillCountNotifier extends BaseNotifier {
                 return null;
 
             case "completion":
-                if ("Gauntlet".equalsIgnoreCase(boss))
-                    return "Crystalline Hunllef";
-                if ("Corrupted Gauntlet".equalsIgnoreCase(boss))
-                    return "Corrupted Hunllef";
+                if (KillCountService.GAUNTLET_NAME.equalsIgnoreCase(boss))
+                    return KillCountService.GAUNTLET_BOSS;
+                if (KillCountService.CG_NAME.equalsIgnoreCase(boss))
+                    return KillCountService.CG_BOSS;
                 return null;
 
             case "kill":
diff --git a/src/main/java/dinkplugin/notifiers/LootNotifier.java b/src/main/java/dinkplugin/notifiers/LootNotifier.java
index 3e4cc82..9d588bf 100644
--- a/src/main/java/dinkplugin/notifiers/LootNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/LootNotifier.java
@@ -136,7 +136,8 @@ public class LootNotifier extends BaseNotifier {
                 return;
             }
 
-            this.handleNotify(lootReceived.getItems(), lootReceived.getName(), lootReceived.getType());
+            String source = killCountService.isCorruptedGauntlet(lootReceived) ? KillCountService.CG_NAME : lootReceived.getName();
+            this.handleNotify(lootReceived.getItems(), source, lootReceived.getType());
         } else if (lootReceived.getType() == LootRecordType.NPC && "The Whisperer".equalsIgnoreCase(lootReceived.getName())) {
             // Special case: upstream fires LootReceived for the whisperer, but not NpcLootReceived
             this.handleNotify(lootReceived.getItems(), lootReceived.getName(), lootReceived.getType());
diff --git a/src/main/java/dinkplugin/util/KillCountService.java b/src/main/java/dinkplugin/util/KillCountService.java
index afa3445..d31beff 100644
--- a/src/main/java/dinkplugin/util/KillCountService.java
+++ b/src/main/java/dinkplugin/util/KillCountService.java
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.Nullable;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Map;
 import java.util.OptionalDouble;
 import java.util.concurrent.ScheduledExecutorService;
@@ -35,6 +36,9 @@ import java.util.concurrent.TimeUnit;
 @Singleton
 public class KillCountService {
 
+    public static final String GAUNTLET_NAME = "Gauntlet", GAUNTLET_BOSS = "Crystalline Hunllef";
+    public static final String CG_NAME = "Corrupted Gauntlet", CG_BOSS = "Corrupted Hunllef";
+
     private static final String RL_CHAT_CMD_PLUGIN_NAME = ChatCommandsPlugin.class.getSimpleName().toLowerCase();
     private static final String RL_LOOT_PLUGIN_NAME = LootTrackerPlugin.class.getSimpleName().toLowerCase();
 
@@ -74,6 +78,10 @@ public class KillCountService {
         }
 
         String name = npc.getName();
+        if (GAUNTLET_BOSS.equals(name) || CG_BOSS.equals(name)) {
+            // already handled by onGameMessage
+            return;
+        }
         if (name != null) {
             this.incrementKills(LootRecordType.NPC, name, event.getItems());
         }
@@ -102,7 +110,8 @@ public class KillCountService {
         }
 
         if (increment) {
-            this.incrementKills(event.getType(), event.getName(), event.getItems());
+            String source = isCorruptedGauntlet(event) ? CG_NAME : event.getName();
+            this.incrementKills(event.getType(), source, event.getItems());
         }
     }
 
@@ -125,6 +134,17 @@ public class KillCountService {
             String cacheKey = getCacheKey(LootRecordType.UNKNOWN, boss);
             killCounts.asMap().merge(cacheKey, kc - 1, Math::max);
 
+            if (boss.equals(GAUNTLET_BOSS) || boss.equals(CG_BOSS)) {
+                // populate lastDrop for isCorruptedGauntlet to function
+                this.lastDrop = new Drop(boss, LootRecordType.EVENT, Collections.emptyList());
+
+                if (!ConfigUtil.isPluginDisabled(configManager, RL_LOOT_PLUGIN_NAME)) {
+                    // onLoot will already increment kc, no need to schedule task below.
+                    // this early return also simplifies our test code
+                    return;
+                }
+            }
+
             // However: we don't know if boss message appeared before/after the loot event.
             // If after, we should store kc. If before, we should store kc - 1.
             // Given this uncertainty, we wait so that the loot event has passed, and then we can store latest kc.
@@ -134,6 +154,17 @@ public class KillCountService {
         });
     }
 
+    /**
+     * @param event a loot received event that was just fired
+     * @return whether the event represents corrupted gauntlet
+     * @apiNote Useful to distinguish normal vs. corrupted gauntlet since the base loot tracker plugin does not,
+     * which was <a href="https://github.com/pajlads/DinkPlugin/issues/469">reported</a> to our issue tracker.
+     */
+    public boolean isCorruptedGauntlet(@NotNull LootReceived event) {
+        return event.getType() == LootRecordType.EVENT && lastDrop != null && "The Gauntlet".equals(event.getName())
+            && (CG_NAME.equals(lastDrop.getSource()) || CG_BOSS.equals(lastDrop.getSource()));
+    }
+
     @Nullable
     public Integer getKillCount(LootRecordType type, String sourceName) {
         if (sourceName == null) return null;
@@ -227,6 +258,8 @@ public class KillCountService {
             case PLAYER:
                 return "player_" + sourceName;
             default:
+                if ("The Gauntlet".equals(sourceName)) return GAUNTLET_BOSS;
+                if (CG_NAME.equals(sourceName)) return CG_BOSS;
                 return sourceName;
         }
     }


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
index bb4d719..bc8644e 100644
--- a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
@@ -652,6 +652,69 @@ class LootNotifierTest extends MockedNotifierTest {
         verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
     }
 
+    @Test
+    void testNotifyGauntlet() {
+        // prepare data
+        int kc = 123;
+        int quantity = 24;
+        String total = QuantityFormatter.quantityToStackSize(quantity * RUBY_PRICE);
+        String source = "The Gauntlet";
+        List<ItemStack> items = List.of(new ItemStack(ItemID.RUBY, quantity, null));
+
+        // fire events
+        killCountService.onGameMessage(String.format("Your Gauntlet completion count is: %d.", kc));
+        plugin.onLootReceived(new LootReceived(source, -1, LootRecordType.EVENT, items, 1));
+
+        // verify notification message
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(
+                    Template.builder()
+                        .template(String.format("%s has looted: %d x {{ruby}} (%s) from {{source}} for %s gp", PLAYER_NAME, quantity, total, total))
+                        .replacement("{{ruby}}", Replacements.ofWiki("Ruby"))
+                        .replacement("{{source}}", Replacements.ofWiki(source))
+                        .build()
+                )
+                .extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, kc, null))
+                .type(NotificationType.LOOT)
+                .build()
+        );
+    }
+
+    @Test
+    void testNotifyCorruptedGauntlet() {
+        // prepare data
+        int kc = 123;
+        int quantity = 24;
+        String total = QuantityFormatter.quantityToStackSize(quantity * RUBY_PRICE);
+        String source = "The Gauntlet";
+        String realSource = "Corrupted Gauntlet";
+        List<ItemStack> items = List.of(new ItemStack(ItemID.RUBY, quantity, null));
+
+        // fire events
+        killCountService.onGameMessage(String.format("Your Corrupted Gauntlet completion count is: %d.", kc));
+        plugin.onLootReceived(new LootReceived(source, -1, LootRecordType.EVENT, items, 1));
+
+        // verify notification message
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(
+                    Template.builder()
+                        .template(String.format("%s has looted: %d x {{ruby}} (%s) from {{source}} for %s gp", PLAYER_NAME, quantity, total, total))
+                        .replacement("{{ruby}}", Replacements.ofWiki("Ruby"))
+                        .replacement("{{source}}", Replacements.ofWiki(realSource))
+                        .build()
+                )
+                .extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), realSource, LootRecordType.EVENT, kc, null))
+                .type(NotificationType.LOOT)
+                .build()
+        );
+    }
+
     @Test
     void testDisabled() {
         // disable notifier