Repository: pajlads/DinkPlugin
Pull request #329 (2023-10-08 13:21:53)

fix: stop treating Quest speedrun personal best ties as personal bests
We now rely on the chat message instead of comparing the times
This means we only fire for new personal bests



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69cbb70..897df1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,9 @@
 ## Unreleased
 
+- Minor: Add key in the Speedrun notifier extra object for whether the run is a personal best or not. (#329)
 - Minor: Indicate in pet notification metadata when a pet was previously owned but lost. (#314)
 - Minor: Support wildcards in loot item name filters. (#312)
+- Bugfix: Don't treat personal best ties as personal bests for Speedrun notifications. (#329)
 - Bugfix: Classify Gauntlet deaths as safe, unless the player is a hardcore ironman. (#327)
 - Bugfix: Classify deaths in Tombs of Abascut as safe or dangerous depending on the attempt invocations. (#317)
 - Dev: Update gradle wrapper to v8.4 minor version. (#328)
diff --git a/README.md b/README.md
index 87ec70a..f5a8304 100644
--- a/README.md
+++ b/README.md
@@ -544,7 +544,8 @@ Note: `petName` is only included if the game sent it to your chat via untradeabl
   "extra": {
     "questName": "Cook's Assistant",
     "personalBest": "1:13.20",
-    "currentTime": "1:13.20"
+    "currentTime": "1:13.20",
+    "isPersonalBest": true
   },
   "type": "SPEEDRUN"
 }
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index 04a0c09..c054c53 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -105,6 +105,7 @@ public class DinkPlugin extends Plugin {
         slayerNotifier.reset();
         killCountNotifier.reset();
         groupStorageNotifier.reset();
+        speedrunNotifier.reset();
     }
 
     @Provides
@@ -179,6 +180,7 @@ public class DinkPlugin extends Plugin {
                 killCountNotifier.onGameMessage(chatMessage);
                 combatTaskNotifier.onGameMessage(chatMessage);
                 deathNotifier.onGameMessage(chatMessage);
+                speedrunNotifier.onGameMessage(chatMessage);
                 break;
 
             case FRIENDSCHATNOTIFICATION:
diff --git a/src/main/java/dinkplugin/notifiers/SpeedrunNotifier.java b/src/main/java/dinkplugin/notifiers/SpeedrunNotifier.java
index a8908a0..9370499 100644
--- a/src/main/java/dinkplugin/notifiers/SpeedrunNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/SpeedrunNotifier.java
@@ -4,14 +4,13 @@ import dinkplugin.message.NotificationBody;
 import dinkplugin.message.NotificationType;
 import dinkplugin.message.templating.Replacements;
 import dinkplugin.message.templating.Template;
+import dinkplugin.notifiers.data.SpeedrunNotificationData;
 import dinkplugin.util.QuestUtils;
 import dinkplugin.util.TimeUtils;
 import dinkplugin.util.Utils;
-import dinkplugin.notifiers.data.SpeedrunNotificationData;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.events.WidgetLoaded;
 import net.runelite.api.widgets.Widget;
-import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.VisibleForTesting;
 
 import java.time.Duration;
@@ -22,6 +21,7 @@ public class SpeedrunNotifier extends BaseNotifier {
     static final @VisibleForTesting int SPEEDRUN_COMPLETED_QUEST_NAME_CHILD_ID = 4;
     static final @VisibleForTesting int SPEEDRUN_COMPLETED_DURATION_CHILD_ID = 10;
     static final @VisibleForTesting int SPEEDRUN_COMPLETED_PB_CHILD_ID = 12;
+    private boolean isPersonalBest = false;
 
     @Override
     public boolean isEnabled() {
@@ -34,6 +34,10 @@ public class SpeedrunNotifier extends BaseNotifier {
         return config.speedrunWebhook();
     }
 
+    public void reset() {
+        isPersonalBest = false;
+    }
+
     public void onWidgetLoaded(WidgetLoaded event) {
         if (event.getGroupId() == SPEEDRUN_COMPLETED_GROUP_ID && isEnabled()) {
             Widget questName = client.getWidget(SPEEDRUN_COMPLETED_GROUP_ID, SPEEDRUN_COMPLETED_QUEST_NAME_CHILD_ID);
@@ -48,16 +52,13 @@ public class SpeedrunNotifier extends BaseNotifier {
     }
 
     private void attemptNotify(String questName, String duration, String pb) {
-        Duration bestTime = TimeUtils.parseTime(pb);
-        Duration currentTime = TimeUtils.parseTime(duration);
-        boolean isPb = bestTime.compareTo(currentTime) >= 0;
-        if (!isPb && config.speedrunPBOnly()) {
+        if (!isPersonalBest && config.speedrunPBOnly()) {
             return;
         }
 
         // pb or notifying on non-pb; take the right string and format placeholders
         Template notifyMessage = Template.builder()
-            .template(isPb ? config.speedrunPBMessage() : config.speedrunMessage())
+            .template(isPersonalBest ? config.speedrunPBMessage() : config.speedrunMessage())
             .replacementBoundary("%")
             .replacement("%USERNAME%", Replacements.ofText(Utils.getPlayerName(client)))
             .replacement("%QUEST%", Replacements.ofWiki(questName))
@@ -65,10 +66,25 @@ public class SpeedrunNotifier extends BaseNotifier {
             .replacement("%BEST%", Replacements.ofText(pb))
             .build();
 
+        // Reformat the durations for the extra object
+        Duration bestTime = TimeUtils.parseTime(pb);
+        Duration currentTime = TimeUtils.parseTime(duration);
+
         createMessage(config.speedrunSendImage(), NotificationBody.builder()
             .text(notifyMessage)
-            .extra(new SpeedrunNotificationData(questName, bestTime.toString(), currentTime.toString()))
+            .extra(new SpeedrunNotificationData(questName, bestTime.toString(), currentTime.toString(), isPersonalBest))
             .type(NotificationType.SPEEDRUN)
             .build());
+        this.reset();
+    }
+
+    public void onGameMessage(String chatMessage) {
+        if (!isEnabled()) {
+            return;
+        }
+
+        if (chatMessage.startsWith("Speedrun duration: ")) {
+            isPersonalBest = chatMessage.endsWith(" (new personal best)");
+        }
     }
 }
diff --git a/src/main/java/dinkplugin/notifiers/data/SpeedrunNotificationData.java b/src/main/java/dinkplugin/notifiers/data/SpeedrunNotificationData.java
index 2091ee8..c666d5b 100644
--- a/src/main/java/dinkplugin/notifiers/data/SpeedrunNotificationData.java
+++ b/src/main/java/dinkplugin/notifiers/data/SpeedrunNotificationData.java
@@ -2,11 +2,32 @@ package dinkplugin.notifiers.data;
 
 import lombok.EqualsAndHashCode;
 import lombok.Value;
+import org.jetbrains.annotations.NotNull;
 
 @Value
 @EqualsAndHashCode(callSuper = false)
 public class SpeedrunNotificationData extends NotificationData {
+    /**
+     * The name of the quest (e.g. "Cook's Assistant")
+     */
+    @NotNull
     String questName;
+
+    /**
+     * The player's personal best of this quest
+     */
+    @NotNull
     String personalBest;
+
+    /**
+     * The time it took the player to finish the quest
+     */
+    @NotNull
     String currentTime;
+
+    /**
+     * Denotes whether this run was a new personal best or not.
+     * If the player ties with their previous personal best, this will be set to false.
+     */
+    boolean isPersonalBest;
 }


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/SpeedrunNotifierTest.java b/src/test/java/dinkplugin/notifiers/SpeedrunNotifierTest.java
index 015f112..ffa2473 100644
--- a/src/test/java/dinkplugin/notifiers/SpeedrunNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/SpeedrunNotifierTest.java
@@ -28,11 +28,26 @@ import static org.mockito.Mockito.when;
 class SpeedrunNotifierTest extends MockedNotifierTest {
 
     private static final String QUEST_NAME = "Cook's Assistant";
+    private static final String PERSONAL_BEST = "1:30.25";
 
     @Bind
     @InjectMocks
     SpeedrunNotifier notifier;
 
+    private static Template buildTemplate(String quest, String time) {
+        return Template.builder()
+            .template(String.format("%s has beat their PB of {{quest}} with a time of %s", PLAYER_NAME, time))
+            .replacement("{{quest}}", Replacements.ofWiki(quest))
+            .build();
+    }
+
+    private static Template buildNonPersonalBestTemplate(String quest, String time) {
+        return Template.builder()
+            .template(String.format("%s has just finished a speedrun of {{quest}} with a time of %s (their PB is %s)", PLAYER_NAME, time, PERSONAL_BEST))
+            .replacement("{{quest}}", Replacements.ofWiki(quest))
+            .build();
+    }
+
     @Override
     @BeforeEach
     protected void setUp() {
@@ -43,6 +58,7 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
         when(config.speedrunPBOnly()).thenReturn(true);
         when(config.speedrunSendImage()).thenReturn(false);
         when(config.speedrunPBMessage()).thenReturn("%USERNAME% has beat their PB of %QUEST% with a time of %TIME%");
+        when(config.speedrunMessage()).thenReturn("%USERNAME% has just finished a speedrun of %QUEST% with a time of %TIME% (their PB is %BEST%)");
 
         // init common widget mocks
         Widget quest = mock(Widget.class);
@@ -51,7 +67,7 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
 
         Widget pb = mock(Widget.class);
         when(client.getWidget(SPEEDRUN_COMPLETED_GROUP_ID, SPEEDRUN_COMPLETED_PB_CHILD_ID)).thenReturn(pb);
-        when(pb.getText()).thenReturn("1:30.25");
+        when(pb.getText()).thenReturn(PERSONAL_BEST);
     }
 
     @Test
@@ -64,6 +80,7 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
         when(time.getText()).thenReturn(latest);
 
         // fire fake event
+        notifier.onGameMessage(String.format("Speedrun duration: %s (new personal best)", latest));
         plugin.onWidgetLoaded(event());
 
         // check notification message
@@ -72,14 +89,40 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
             false,
             NotificationBody.builder()
                 .text(buildTemplate(QUEST_NAME, latest))
-                .extra(new SpeedrunNotificationData(QUEST_NAME, Duration.ofMinutes(1).plusSeconds(30).plusMillis(250).toString(), Duration.ofMinutes(1).plusSeconds(15).plusMillis(300).toString()))
+                .extra(new SpeedrunNotificationData(QUEST_NAME, Duration.ofMinutes(1).plusSeconds(30).plusMillis(250).toString(), Duration.ofMinutes(1).plusSeconds(15).plusMillis(300).toString(), true))
+                .type(NotificationType.SPEEDRUN)
+                .build()
+        );
+    }
+
+    @Test
+    void testNotifyNotPersonalBest() {
+        String latest = "1:40.30";
+
+        // mock widget
+        Widget time = mock(Widget.class);
+        when(config.speedrunPBOnly()).thenReturn(false);
+        when(client.getWidget(SPEEDRUN_COMPLETED_GROUP_ID, SPEEDRUN_COMPLETED_DURATION_CHILD_ID)).thenReturn(time);
+        when(time.getText()).thenReturn(latest);
+
+        // fire fake event
+        notifier.onGameMessage(String.format("Speedrun duration: %s", latest));
+        plugin.onWidgetLoaded(event());
+
+        // check notification message
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(buildNonPersonalBestTemplate(QUEST_NAME, latest))
+                .extra(new SpeedrunNotificationData(QUEST_NAME, Duration.ofMinutes(1).plusSeconds(30).plusMillis(250).toString(), Duration.ofMinutes(1).plusSeconds(40).plusMillis(300).toString(), false))
                 .type(NotificationType.SPEEDRUN)
                 .build()
         );
     }
 
     @Test
-    void testIgnore() {
+    void testIgnoreNonPersonalBest() {
         String latest = "1:45.30";
 
         // mock widget
@@ -88,6 +131,7 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
         when(time.getText()).thenReturn(latest);
 
         // fire fake event
+        notifier.onGameMessage(String.format("Speedrun duration: %s", latest));
         plugin.onWidgetLoaded(event());
 
         // ensure no notification
@@ -100,11 +144,13 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
         when(config.notifySpeedrun()).thenReturn(false);
 
         // mock widget
+        String latest = "1:15.30";
         Widget time = mock(Widget.class);
         when(client.getWidget(SPEEDRUN_COMPLETED_GROUP_ID, SPEEDRUN_COMPLETED_DURATION_CHILD_ID)).thenReturn(time);
-        when(time.getText()).thenReturn("1:15.30");
+        when(time.getText()).thenReturn(latest);
 
         // fire fake event
+        notifier.onGameMessage(String.format("Speedrun duration: %s (new personal best)", latest));
         plugin.onWidgetLoaded(event());
 
         // ensure no notification
@@ -124,6 +170,7 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
         when(time.getText()).thenReturn(latest);
 
         // fire fake event
+        notifier.onGameMessage(String.format("Speedrun duration: %s (new personal best)", latest));
         plugin.onWidgetLoaded(event());
 
         // check notification message
@@ -132,7 +179,7 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
             false,
             NotificationBody.builder()
                 .text(buildTemplate(QUEST_NAME, latest))
-                .extra(new SpeedrunNotificationData(QUEST_NAME, Duration.ofMinutes(1).plusSeconds(30).plusMillis(250).toString(), Duration.ofMinutes(1).plusSeconds(15).plusMillis(300).toString()))
+                .extra(new SpeedrunNotificationData(QUEST_NAME, Duration.ofMinutes(1).plusSeconds(30).plusMillis(250).toString(), Duration.ofMinutes(1).plusSeconds(15).plusMillis(300).toString(), true))
                 .type(NotificationType.SPEEDRUN)
                 .build()
         );
@@ -140,6 +187,8 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
 
     @Test
     void testIgnoreDenyList() {
+        String latest = "1:15.30";
+
         // configure deny list
         when(config.filteredNames()).thenReturn(PLAYER_NAME);
         settingsManager.init();
@@ -147,9 +196,10 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
         // mock widget
         Widget time = mock(Widget.class);
         when(client.getWidget(SPEEDRUN_COMPLETED_GROUP_ID, SPEEDRUN_COMPLETED_DURATION_CHILD_ID)).thenReturn(time);
-        when(time.getText()).thenReturn("1:15.30");
+        when(time.getText()).thenReturn(latest);
 
         // fire fake event
+        notifier.onGameMessage(String.format("Speedrun duration: %s (new personal best)", latest));
         plugin.onWidgetLoaded(event());
 
         // ensure no notification
@@ -161,11 +211,4 @@ class SpeedrunNotifierTest extends MockedNotifierTest {
         event.setGroupId(SPEEDRUN_COMPLETED_GROUP_ID);
         return event;
     }
-
-    private static Template buildTemplate(String quest, String time) {
-        return Template.builder()
-            .template(String.format("%s has beat their PB of {{quest}} with a time of %s", PLAYER_NAME, time))
-            .replacement("{{quest}}", Replacements.ofWiki(quest))
-            .build();
-    }
 }