Repository: pajlads/DinkPlugin
Pull request #157 (2023-02-03 10:06:29)

feat: parse diary completion from message box
Should help with #93. Thanks to DvH for the idea



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4939c4c..681651b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 - Minor: Clean up setting tooltips that stretched too wide, making them difficult to read. (#151)
 - Minor: Add warning when in-game kill count chat spam filter is enabled. (#154)
 - Minor: Include pet name in webhooks when available in chat via clan, collection log, or untradeable drop notifications. (#149, #153)
+- Bugfix: Improve identification of diary completions via message box events. (#157)
 - Bugfix: Report correct item quantity from unsired loot. (#147)
 - Dev: Improve thread-safety of notifiers. (#156)
 
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index 5794f02..faf4236 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -152,6 +152,7 @@ public class DinkPlugin extends Plugin {
                 break;
 
             case MESBOX:
+                diaryNotifier.onMessageBox(chatMessage);
                 gambleNotifier.onMesBoxNotification(chatMessage);
                 break;
 
diff --git a/src/main/java/dinkplugin/notifiers/DiaryNotifier.java b/src/main/java/dinkplugin/notifiers/DiaryNotifier.java
index 9749456..d4f1abb 100644
--- a/src/main/java/dinkplugin/notifiers/DiaryNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/DiaryNotifier.java
@@ -14,16 +14,21 @@ import org.apache.commons.lang3.tuple.Pair;
 
 import javax.inject.Singleton;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import static dinkplugin.domain.AchievementDiary.DIARIES;
 
 @Slf4j
 @Singleton
 public class DiaryNotifier extends BaseNotifier {
+    private static final Pattern COMPLETION_REGEX = Pattern.compile("Congratulations! You have completed all of the (?<difficulty>.+) tasks in the (?<area>.+) area");
     private final Map<Integer, Integer> diaryCompletionById = new ConcurrentHashMap<>();
     private final AtomicInteger initDelayTicks = new AtomicInteger();
+    private final AtomicInteger cooldownTicks = new AtomicInteger();
 
     @Override
     public boolean isEnabled() {
@@ -38,6 +43,7 @@ public class DiaryNotifier extends BaseNotifier {
     public void reset() {
         this.diaryCompletionById.clear();
         this.initDelayTicks.set(0);
+        this.cooldownTicks.set(0);
     }
 
     public void onGameState(GameStateChanged event) {
@@ -49,6 +55,7 @@ public class DiaryNotifier extends BaseNotifier {
         if (client.getGameState() != GameState.LOGGED_IN)
             return;
 
+        cooldownTicks.getAndUpdate(i -> Math.max(i - 1, 0));
         int ticks = initDelayTicks.getAndUpdate(i -> Math.max(i - 1, 0));
         if (ticks > 0) {
             if (ticks == 1) {
@@ -60,6 +67,39 @@ public class DiaryNotifier extends BaseNotifier {
         }
     }
 
+    public void onMessageBox(String message) {
+        if (!isEnabled()) return;
+
+        Matcher matcher = COMPLETION_REGEX.matcher(message);
+        if (matcher.find()) {
+            String difficultyStr = matcher.group("difficulty");
+            AchievementDiary.Difficulty difficulty;
+            try {
+                difficulty = AchievementDiary.Difficulty.valueOf(difficultyStr.toUpperCase());
+            } catch (IllegalArgumentException e) {
+                log.warn("Failed to match diary difficulty: {}", difficultyStr);
+                return;
+            }
+
+            String area = matcher.group("area");
+            Optional<Integer> found = DIARIES.entrySet().stream()
+                .filter(e -> e.getValue().getRight() == difficulty && e.getValue().getLeft().equals(area))
+                .map(Map.Entry::getKey)
+                .findAny();
+            if (found.isPresent()) {
+                int varbitId = found.get();
+                if (isComplete(varbitId, 1)) {
+                    diaryCompletionById.put(varbitId, 1);
+                } else {
+                    diaryCompletionById.put(varbitId, 2);
+                }
+                handle(area, difficulty);
+            } else {
+                log.warn("Failed to match diary area: {}", area);
+            }
+        }
+    }
+
     public void onVarbitChanged(VarbitChanged event) {
         int id = event.getVarbitId();
         if (id < 0) return;
@@ -101,6 +141,11 @@ public class DiaryNotifier extends BaseNotifier {
     }
 
     private void handle(String area, AchievementDiary.Difficulty difficulty) {
+        if (cooldownTicks.getAndSet(2) > 0) {
+            log.debug("Skipping diary completion during cooldown: {} {}", difficulty, area);
+            return;
+        }
+
         int total = getTotalCompleted();
         String player = Utils.getPlayerName(client);
         String message = StringUtils.replaceEach(


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java b/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java
index e9e5cfe..caf9aa4 100644
--- a/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java
@@ -11,6 +11,7 @@ import net.runelite.api.events.VarbitChanged;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
+import org.mockito.Mockito;
 
 import java.util.stream.IntStream;
 
@@ -94,6 +95,63 @@ class DiaryNotifierTest extends MockedNotifierTest {
         );
     }
 
+    @Test
+    void testNotifyKaramjaMessageBox() {
+        // initially many diary completions
+        when(client.getVarbitValue(anyInt())).thenReturn(1);
+        int total = AchievementDiary.DIARIES.size() - 3;
+
+        // perform enough ticks to trigger diary initialization
+        IntStream.range(0, 16).forEach(i -> notifier.onTick());
+
+        // trigger diary completion
+        notifier.onMessageBox("Congratulations! You have completed all of the hard tasks in the Karamja area. Speak to Pirate Jackie the Fruit to claim your reward.");
+
+        // verify notification message
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(String.format("%s has completed the %s %s Diary, for a total of %d", PLAYER_NAME, AchievementDiary.Difficulty.HARD, "Karamja", total + 1))
+                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1))
+                .type(NotificationType.ACHIEVEMENT_DIARY)
+                .playerName(PLAYER_NAME)
+                .build()
+        );
+    }
+
+    @Test
+    void testNotifyCooldown() {
+        // initially many diary completions
+        when(client.getVarbitValue(anyInt())).thenReturn(1);
+        int total = AchievementDiary.DIARIES.size() - 3;
+
+        // perform enough ticks to trigger diary initialization
+        IntStream.range(0, 16).forEach(i -> notifier.onTick());
+
+        // trigger diary completion
+        int id = Varbits.DIARY_KARAMJA_HARD;
+        plugin.onVarbitChanged(event(id, 2));
+
+        // verify notification message
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(String.format("%s has completed the %s %s Diary, for a total of %d", PLAYER_NAME, AchievementDiary.Difficulty.HARD, "Karamja", total + 1))
+                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1))
+                .type(NotificationType.ACHIEVEMENT_DIARY)
+                .playerName(PLAYER_NAME)
+                .build()
+        );
+
+        // trigger message box
+        notifier.onMessageBox("Congratulations! You have completed all of the hard tasks in the Karamja area. Speak to Pirate Jackie the Fruit to claim your reward.");
+
+        // ensure no notification
+        Mockito.verifyNoMoreInteractions(messageHandler);
+    }
+
     @Test
     void testIgnoreKaramja() {
         // initially 0 diary completions