Repository: pajlads/DinkPlugin
Pull request #331 (2023-10-14 00:47:44)

feat: include task count in diary notifications
Follow-up from https://github.com/pajlads/DinkPlugin/pull/321#issuecomment-1742133836

![image](https://github.com/pajlads/DinkPlugin/assets/8106344/b9c2f38e-c526-4138-8580-47866e4929b7)


pajlada: finished Desert Medium Diary

![image](https://github.com/pajlads/DinkPlugin/assets/962989/c4a42ad6-f5ab-4f17-884c-6f76073d6292)


```json
{
  "type": "ACHIEVEMENT_DIARY",
  "playerName": "pajdonk",
  "accountType": "NORMAL",
  "dinkAccountHash": "9748bae266b7923da080b8e5fc9f3b8855a7af49321bbf909b5a8ad9",
  "extra": {
    "area": "Desert",
    "difficulty": "MEDIUM",
    "total": 18,
    "tasksCompleted": 247,
    "tasksTotal": 492,
    "areaTasksCompleted": 25,
    "areaTasksTotal": 39
  },
  "discordUser": {
    "id": "85699361769553920",
    "name": "pajlada",
    "avatarHash": "e75df3dbe6cb04b3c9f0e090b3adb190"
  },
  "embeds": [
    {
      "title": "Achievement Diary",
      "description": "pajdonk has completed the Medium [Desert](https://oldschool.runescape.wiki/w/Special:Search?search=Desert%20Diary) Achievement Diary, for a total of 18 diaries completed",
      "author": {
        "name": "pajdonk",
        "url": "https://secure.runescape.com/m=hiscore_oldschool/hiscorepersonal?user1=pajdonk"
      },
      "color": 15990936,
      "image": {
        "url": "attachment://achievementDiaryImage.png"
      },
      "thumbnail": {
        "url": "https://oldschool.runescape.wiki/images/Achievement_Diaries_icon.png"
      },
      "fields": [
        {
          "name": "Desert Progress",
          "value": "```\n25/39 (64.1%)\n```",
          "inline": true
        },
        {
          "name": "Overall Progress",
          "value": "```\n247/492 (50.2%)\n```",
          "inline": true
        }
      ],
      "footer": {
        "text": "Powered by Dink",
        "icon_url": "https://github.com/pajlads/DinkPlugin/raw/master/icon.png"
      },
      "timestamp": "2023-10-25T21:46:14.913234269Z"
    }
  ]
}
```

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57378a3..09496b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Unreleased
 
+- Minor: Add task progress metadata for diary notifications. (#331)
 - Minor: Use boss chat message for latest kill count in loot notifications. (#324)
 - Minor: Include hashed account unique identifier in notification metadata. (#334)
 - Minor: Made the default setting for screenshots enabled for consistency across all notifiers. (#330)
diff --git a/README.md b/README.md
index 71f06d5..3357049 100644
--- a/README.md
+++ b/README.md
@@ -486,10 +486,18 @@ If the task completion unlocked rewards for a tier, `%COMPLETED%` will be replac
 
 `%AREA%` will be replaced with the geographic area of the achievement diary tasks (e.g., Varrock)
 
-`%DIFFICULTY%` will be replaced with the level of the achievement diary (e.g., Elite)
+`%DIFFICULTY%` will be replaced with the level of the achievement diary (e.g., Hard)
 
 `%TOTAL%` will be replaced with the total number of achievement diaries completed across all locations and difficulties
 
+`%TASKS_COMPLETE%` will be replaced with the number of tasks completed across all locations and difficulties
+
+`%TASKS_TOTAL%` will be replaced with the total number of tasks possible across all locations and difficulties
+
+`%AREA_TASKS_COMPLETE%` will be replaced with the number of tasks completed within the area
+
+`%AREA_TASKS_TOTAL%` will be replaced with the total number of tasks possible within the area
+
 <details>
   <summary>JSON for Achievement Diary Notifications:</summary>
 
@@ -498,8 +506,12 @@ If the task completion unlocked rewards for a tier, `%COMPLETED%` will be replac
   "content": "%USERNAME% has completed the %DIFFICULTY% %AREA% Achievement Diary, for a total of %TOTAL% diaries completed",
   "extra": {
     "area": "Varrock",
-    "difficulty": "ELITE",
-    "total": 36
+    "difficulty": "HARD",
+    "total": 15,
+    "tasksCompleted": 152,
+    "tasksTotal": 492,
+    "areaTasksCompleted": 37,
+    "areaTasksTotal": 42
   },
   "type": "ACHIEVEMENT_DIARY"
 }
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index 2da7f14..b6c9c1b 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -1338,7 +1338,11 @@ public interface DinkPluginConfig extends Config {
             "Use %USERNAME% to insert your username<br/>" +
             "Use %DIFFICULTY% to insert the diary difficulty<br/>" +
             "Use %AREA% to insert the diary area<br/>" +
-            "Use %TOTAL% to insert the total diaries completed",
+            "Use %TOTAL% to insert the total diaries completed<br/>" +
+            "Use %TASKS_COMPLETE% to insert the tasks completed across all diaries<br/>" +
+            "Use %TASKS_TOTAL% to insert the total tasks possible across all diaries<br/>" +
+            "Use %AREA_TASKS_COMPLETE% to insert the tasks completed within the area<br/>" +
+            "Use %AREA_TASKS_TOTAL% to insert the total tasks possible within the area",
         position = 113,
         section = diarySection
     )
diff --git a/src/main/java/dinkplugin/domain/AchievementDiary.java b/src/main/java/dinkplugin/domain/AchievementDiary.java
index 7cf3b8e..ad9e39d 100644
--- a/src/main/java/dinkplugin/domain/AchievementDiary.java
+++ b/src/main/java/dinkplugin/domain/AchievementDiary.java
@@ -4,72 +4,72 @@ import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import net.runelite.api.Varbits;
 import net.runelite.api.annotations.Varbit;
-import org.apache.commons.lang3.tuple.Pair;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 @Getter
 @RequiredArgsConstructor
 public enum AchievementDiary {
-    ARDOUGNE_EASY(Varbits.DIARY_ARDOUGNE_EASY, "Ardougne", Difficulty.EASY),
-    ARDOUGNE_MEDIUM(Varbits.DIARY_ARDOUGNE_MEDIUM, "Ardougne", Difficulty.MEDIUM),
-    ARDOUGNE_HARD(Varbits.DIARY_ARDOUGNE_HARD, "Ardougne", Difficulty.HARD),
-    ARDOUGNE_ELITE(Varbits.DIARY_ARDOUGNE_ELITE, "Ardougne", Difficulty.ELITE),
-    DESERT_EASY(Varbits.DIARY_DESERT_EASY, "Desert", Difficulty.EASY),
-    DESERT_MEDIUM(Varbits.DIARY_DESERT_MEDIUM, "Desert", Difficulty.MEDIUM),
-    DESERT_HARD(Varbits.DIARY_DESERT_HARD, "Desert", Difficulty.HARD),
-    DESERT_ELITE(Varbits.DIARY_DESERT_ELITE, "Desert", Difficulty.ELITE),
-    FALADOR_EASY(Varbits.DIARY_FALADOR_EASY, "Falador", Difficulty.EASY),
-    FALADOR_MEDIUM(Varbits.DIARY_FALADOR_MEDIUM, "Falador", Difficulty.MEDIUM),
-    FALADOR_HARD(Varbits.DIARY_FALADOR_HARD, "Falador", Difficulty.HARD),
-    FALADOR_ELITE(Varbits.DIARY_FALADOR_ELITE, "Falador", Difficulty.ELITE),
-    FREMENNIK_EASY(Varbits.DIARY_FREMENNIK_EASY, "Fremennik", Difficulty.EASY),
-    FREMENNIK_MEDIUM(Varbits.DIARY_FREMENNIK_MEDIUM, "Fremennik", Difficulty.MEDIUM),
-    FREMENNIK_HARD(Varbits.DIARY_FREMENNIK_HARD, "Fremennik", Difficulty.HARD),
-    FREMENNIK_ELITE(Varbits.DIARY_FREMENNIK_ELITE, "Fremennik", Difficulty.ELITE),
-    KANDARIN_EASY(Varbits.DIARY_KANDARIN_EASY, "Kandarin", Difficulty.EASY),
-    KANDARIN_MEDIUM(Varbits.DIARY_KANDARIN_MEDIUM, "Kandarin", Difficulty.MEDIUM),
-    KANDARIN_HARD(Varbits.DIARY_KANDARIN_HARD, "Kandarin", Difficulty.HARD),
-    KANDARIN_ELITE(Varbits.DIARY_KANDARIN_ELITE, "Kandarin", Difficulty.ELITE),
-    KARAMJA_EASY(Varbits.DIARY_KARAMJA_EASY, "Karamja", Difficulty.EASY),
-    KARAMJA_MEDIUM(Varbits.DIARY_KARAMJA_MEDIUM, "Karamja", Difficulty.MEDIUM),
-    KARAMJA_HARD(Varbits.DIARY_KARAMJA_HARD, "Karamja", Difficulty.HARD),
-    KARAMJA_ELITE(Varbits.DIARY_KARAMJA_ELITE, "Karamja", Difficulty.ELITE),
-    KOUREND_EASY(Varbits.DIARY_KOUREND_EASY, "Kourend & Kebos", Difficulty.EASY),
-    KOUREND_MEDIUM(Varbits.DIARY_KOUREND_MEDIUM, "Kourend & Kebos", Difficulty.MEDIUM),
-    KOUREND_HARD(Varbits.DIARY_KOUREND_HARD, "Kourend & Kebos", Difficulty.HARD),
-    KOUREND_ELITE(Varbits.DIARY_KOUREND_ELITE, "Kourend & Kebos", Difficulty.ELITE),
-    LUMBRIDGE_EASY(Varbits.DIARY_LUMBRIDGE_EASY, "Lumbridge & Draynor", Difficulty.EASY),
-    LUMBRIDGE_MEDIUM(Varbits.DIARY_LUMBRIDGE_MEDIUM, "Lumbridge & Draynor", Difficulty.MEDIUM),
-    LUMBRIDGE_HARD(Varbits.DIARY_LUMBRIDGE_HARD, "Lumbridge & Draynor", Difficulty.HARD),
-    LUMBRIDGE_ELITE(Varbits.DIARY_LUMBRIDGE_ELITE, "Lumbridge & Draynor", Difficulty.ELITE),
-    MORYTANIA_EASY(Varbits.DIARY_MORYTANIA_EASY, "Morytania", Difficulty.EASY),
-    MORYTANIA_MEDIUM(Varbits.DIARY_MORYTANIA_MEDIUM, "Morytania", Difficulty.MEDIUM),
-    MORYTANIA_HARD(Varbits.DIARY_MORYTANIA_HARD, "Morytania", Difficulty.HARD),
-    MORYTANIA_ELITE(Varbits.DIARY_MORYTANIA_ELITE, "Morytania", Difficulty.ELITE),
-    VARROCK_EASY(Varbits.DIARY_VARROCK_EASY, "Varrock", Difficulty.EASY),
-    VARROCK_MEDIUM(Varbits.DIARY_VARROCK_MEDIUM, "Varrock", Difficulty.MEDIUM),
-    VARROCK_HARD(Varbits.DIARY_VARROCK_HARD, "Varrock", Difficulty.HARD),
-    VARROCK_ELITE(Varbits.DIARY_VARROCK_ELITE, "Varrock", Difficulty.ELITE),
-    WESTERN_EASY(Varbits.DIARY_WESTERN_EASY, "Western Provinces", Difficulty.EASY),
-    WESTERN_MEDIUM(Varbits.DIARY_WESTERN_MEDIUM, "Western Provinces", Difficulty.MEDIUM),
-    WESTERN_HARD(Varbits.DIARY_WESTERN_HARD, "Western Provinces", Difficulty.HARD),
-    WESTERN_ELITE(Varbits.DIARY_WESTERN_ELITE, "Western Provinces", Difficulty.ELITE),
-    WILDERNESS_EASY(Varbits.DIARY_WILDERNESS_EASY, "Wilderness", Difficulty.EASY),
-    WILDERNESS_MEDIUM(Varbits.DIARY_WILDERNESS_MEDIUM, "Wilderness", Difficulty.MEDIUM),
-    WILDERNESS_HARD(Varbits.DIARY_WILDERNESS_HARD, "Wilderness", Difficulty.HARD),
-    WILDERNESS_ELITE(Varbits.DIARY_WILDERNESS_ELITE, "Wilderness", Difficulty.ELITE);
+    ARDOUGNE_EASY(Varbits.DIARY_ARDOUGNE_EASY, 1, "Ardougne", Difficulty.EASY),
+    ARDOUGNE_MEDIUM(Varbits.DIARY_ARDOUGNE_MEDIUM, 1, "Ardougne", Difficulty.MEDIUM),
+    ARDOUGNE_HARD(Varbits.DIARY_ARDOUGNE_HARD, 1, "Ardougne", Difficulty.HARD),
+    ARDOUGNE_ELITE(Varbits.DIARY_ARDOUGNE_ELITE, 1, "Ardougne", Difficulty.ELITE),
+    DESERT_EASY(Varbits.DIARY_DESERT_EASY, 5, "Desert", Difficulty.EASY),
+    DESERT_MEDIUM(Varbits.DIARY_DESERT_MEDIUM, 5, "Desert", Difficulty.MEDIUM),
+    DESERT_HARD(Varbits.DIARY_DESERT_HARD, 5, "Desert", Difficulty.HARD),
+    DESERT_ELITE(Varbits.DIARY_DESERT_ELITE, 5, "Desert", Difficulty.ELITE),
+    FALADOR_EASY(Varbits.DIARY_FALADOR_EASY, 2, "Falador", Difficulty.EASY),
+    FALADOR_MEDIUM(Varbits.DIARY_FALADOR_MEDIUM, 2, "Falador", Difficulty.MEDIUM),
+    FALADOR_HARD(Varbits.DIARY_FALADOR_HARD, 2, "Falador", Difficulty.HARD),
+    FALADOR_ELITE(Varbits.DIARY_FALADOR_ELITE, 2, "Falador", Difficulty.ELITE),
+    FREMENNIK_EASY(Varbits.DIARY_FREMENNIK_EASY, 3, "Fremennik", Difficulty.EASY),
+    FREMENNIK_MEDIUM(Varbits.DIARY_FREMENNIK_MEDIUM, 3, "Fremennik", Difficulty.MEDIUM),
+    FREMENNIK_HARD(Varbits.DIARY_FREMENNIK_HARD, 3, "Fremennik", Difficulty.HARD),
+    FREMENNIK_ELITE(Varbits.DIARY_FREMENNIK_ELITE, 3, "Fremennik", Difficulty.ELITE),
+    KANDARIN_EASY(Varbits.DIARY_KANDARIN_EASY, 4, "Kandarin", Difficulty.EASY),
+    KANDARIN_MEDIUM(Varbits.DIARY_KANDARIN_MEDIUM, 4, "Kandarin", Difficulty.MEDIUM),
+    KANDARIN_HARD(Varbits.DIARY_KANDARIN_HARD, 4, "Kandarin", Difficulty.HARD),
+    KANDARIN_ELITE(Varbits.DIARY_KANDARIN_ELITE, 4, "Kandarin", Difficulty.ELITE),
+    KARAMJA_EASY(Varbits.DIARY_KARAMJA_EASY, 0, "Karamja", Difficulty.EASY),
+    KARAMJA_MEDIUM(Varbits.DIARY_KARAMJA_MEDIUM, 0, "Karamja", Difficulty.MEDIUM),
+    KARAMJA_HARD(Varbits.DIARY_KARAMJA_HARD, 0, "Karamja", Difficulty.HARD),
+    KARAMJA_ELITE(Varbits.DIARY_KARAMJA_ELITE, 0, "Karamja", Difficulty.ELITE),
+    KOUREND_EASY(Varbits.DIARY_KOUREND_EASY, 11, "Kourend & Kebos", Difficulty.EASY),
+    KOUREND_MEDIUM(Varbits.DIARY_KOUREND_MEDIUM, 11, "Kourend & Kebos", Difficulty.MEDIUM),
+    KOUREND_HARD(Varbits.DIARY_KOUREND_HARD, 11, "Kourend & Kebos", Difficulty.HARD),
+    KOUREND_ELITE(Varbits.DIARY_KOUREND_ELITE, 11, "Kourend & Kebos", Difficulty.ELITE),
+    LUMBRIDGE_EASY(Varbits.DIARY_LUMBRIDGE_EASY, 6, "Lumbridge & Draynor", Difficulty.EASY),
+    LUMBRIDGE_MEDIUM(Varbits.DIARY_LUMBRIDGE_MEDIUM, 6, "Lumbridge & Draynor", Difficulty.MEDIUM),
+    LUMBRIDGE_HARD(Varbits.DIARY_LUMBRIDGE_HARD, 6, "Lumbridge & Draynor", Difficulty.HARD),
+    LUMBRIDGE_ELITE(Varbits.DIARY_LUMBRIDGE_ELITE, 6, "Lumbridge & Draynor", Difficulty.ELITE),
+    MORYTANIA_EASY(Varbits.DIARY_MORYTANIA_EASY, 7, "Morytania", Difficulty.EASY),
+    MORYTANIA_MEDIUM(Varbits.DIARY_MORYTANIA_MEDIUM, 7, "Morytania", Difficulty.MEDIUM),
+    MORYTANIA_HARD(Varbits.DIARY_MORYTANIA_HARD, 7, "Morytania", Difficulty.HARD),
+    MORYTANIA_ELITE(Varbits.DIARY_MORYTANIA_ELITE, 7, "Morytania", Difficulty.ELITE),
+    VARROCK_EASY(Varbits.DIARY_VARROCK_EASY, 8, "Varrock", Difficulty.EASY),
+    VARROCK_MEDIUM(Varbits.DIARY_VARROCK_MEDIUM, 8, "Varrock", Difficulty.MEDIUM),
+    VARROCK_HARD(Varbits.DIARY_VARROCK_HARD, 8, "Varrock", Difficulty.HARD),
+    VARROCK_ELITE(Varbits.DIARY_VARROCK_ELITE, 8, "Varrock", Difficulty.ELITE),
+    WESTERN_EASY(Varbits.DIARY_WESTERN_EASY, 10, "Western Provinces", Difficulty.EASY),
+    WESTERN_MEDIUM(Varbits.DIARY_WESTERN_MEDIUM, 10, "Western Provinces", Difficulty.MEDIUM),
+    WESTERN_HARD(Varbits.DIARY_WESTERN_HARD, 10, "Western Provinces", Difficulty.HARD),
+    WESTERN_ELITE(Varbits.DIARY_WESTERN_ELITE, 10, "Western Provinces", Difficulty.ELITE),
+    WILDERNESS_EASY(Varbits.DIARY_WILDERNESS_EASY, 9, "Wilderness", Difficulty.EASY),
+    WILDERNESS_MEDIUM(Varbits.DIARY_WILDERNESS_MEDIUM, 9, "Wilderness", Difficulty.MEDIUM),
+    WILDERNESS_HARD(Varbits.DIARY_WILDERNESS_HARD, 9, "Wilderness", Difficulty.HARD),
+    WILDERNESS_ELITE(Varbits.DIARY_WILDERNESS_ELITE, 9, "Wilderness", Difficulty.ELITE);
 
-    public static final Map<Integer, Pair<String, Difficulty>> DIARIES = Collections.unmodifiableMap(
-        Arrays.stream(values())
-            .collect(Collectors.toMap(AchievementDiary::getId, diary -> Pair.of(diary.getArea(), diary.getDifficulty())))
+    public static final Map<Integer, AchievementDiary> DIARIES = Collections.unmodifiableMap(
+        Arrays.stream(values()).collect(Collectors.toMap(AchievementDiary::getId, Function.identity()))
     );
 
     @Varbit
     private final int id;
+    private final int areaId;
     private final String area;
     private final Difficulty difficulty;
 
diff --git a/src/main/java/dinkplugin/notifiers/DiaryNotifier.java b/src/main/java/dinkplugin/notifiers/DiaryNotifier.java
index 9af80bd..55e6fd6 100644
--- a/src/main/java/dinkplugin/notifiers/DiaryNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/DiaryNotifier.java
@@ -11,8 +11,9 @@ import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.GameState;
 import net.runelite.api.events.GameStateChanged;
 import net.runelite.api.events.VarbitChanged;
-import org.apache.commons.lang3.tuple.Pair;
+import net.runelite.client.callback.ClientThread;
 
+import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.util.Map;
 import java.util.Optional;
@@ -37,11 +38,24 @@ public class DiaryNotifier extends BaseNotifier {
      */
     static final int TOTAL_TASKS_SCRIPT_ID = 3980;
 
+    /**
+     * @see <a href="https://github.com/Joshua-F/cs2-scripts/blob/master/scripts/%5Bproc,summary_diary_completed%5D.cs2">CS2 Reference</a>
+     */
+    private static final int COMPLETED_AREA_TASKS_SCRIPT_ID = 4072;
+
+    /**
+     * @see <a href="https://github.com/Joshua-F/cs2-scripts/blob/master/scripts/%5Bproc,summary_diary_total%5D.cs2">CS2 Reference</a>
+     */
+    private static final int TOTAL_AREA_TASKS_SCRIPT_ID = 4073;
+
     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();
 
+    @Inject
+    private ClientThread clientThread;
+
     @Override
     public boolean isEnabled() {
         return config.notifyAchievementDiary() && super.isEnabled();
@@ -94,19 +108,18 @@ public class DiaryNotifier extends BaseNotifier {
             }
 
             String area = matcher.group("area").trim();
-            Optional<Pair<Integer, String>> found = DIARIES.entrySet().stream()
-                .filter(e -> e.getValue().getRight() == difficulty && Utils.containsEither(e.getValue().getLeft(), area))
-                .findAny()
-                .map(entry -> Pair.of(entry.getKey(), entry.getValue().getLeft()));
+            Optional<AchievementDiary> found = DIARIES.values().stream()
+                .filter(e -> e.getDifficulty() == difficulty && Utils.containsEither(e.getArea(), area))
+                .findAny();
             if (found.isPresent()) {
-                Pair<Integer, String> entry = found.get();
-                int varbitId = entry.getKey();
+                AchievementDiary diary = found.get();
+                int varbitId = diary.getId();
                 if (isComplete(varbitId, 1)) {
                     diaryCompletionById.put(varbitId, 1);
                 } else {
                     diaryCompletionById.put(varbitId, 2);
                 }
-                handle(entry.getValue(), difficulty);
+                clientThread.invokeLater(() -> handle(diary)); // 20ms delay to run scripts cleanly
             } else {
                 log.warn("Failed to match diary area: {}", area);
             }
@@ -116,13 +129,13 @@ public class DiaryNotifier extends BaseNotifier {
     public void onVarbitChanged(VarbitChanged event) {
         int id = event.getVarbitId();
         if (id < 0) return;
-        Pair<String, AchievementDiary.Difficulty> diary = DIARIES.get(id);
+        AchievementDiary diary = DIARIES.get(id);
         if (diary == null) return;
         if (!super.isEnabled()) return;
         if (diaryCompletionById.isEmpty()) {
             if (client.getGameState() == GameState.LOGGED_IN && isComplete(id, event.getValue())) {
                 // this log only occurs in exceptional circumstances (i.e., completion within seconds of logging in or enabling the plugin)
-                log.info("Skipping {} {} diary completion that occurred before map initialization", diary.getRight(), diary.getLeft());
+                log.info("Skipping {} {} diary completion that occurred before map initialization", diary.getDifficulty(), diary.getArea());
             }
             return;
         }
@@ -131,49 +144,63 @@ public class DiaryNotifier extends BaseNotifier {
         Integer previous = diaryCompletionById.get(id);
         if (previous == null) {
             // this log should not occur, barring a jagex oddity
-            log.warn("Resetting since {} {} diary was not initialized with a valid value; received new value of {}", diary.getRight(), diary.getLeft(), value);
+            log.warn("Resetting since {} {} diary was not initialized with a valid value; received new value of {}", diary.getDifficulty(), diary.getArea(), value);
             reset();
         } else if (value < previous) {
             // this log should not occur, barring a jagex/runelite oddity
-            log.info("Resetting since it appears {} {} diary has lost progress from {}; received new value of {}", diary.getRight(), diary.getLeft(), previous, value);
+            log.info("Resetting since it appears {} {} diary has lost progress from {}; received new value of {}", diary.getDifficulty(), diary.getArea(), previous, value);
             reset();
         } else if (value > previous) {
             diaryCompletionById.put(id, value);
 
             if (isComplete(id, value)) {
-                if (checkDifficulty(diary.getRight())) {
-                    handle(diary.getLeft(), diary.getRight());
+                if (checkDifficulty(diary.getDifficulty())) {
+                    clientThread.invokeLater(() -> handle(diary)); // 20ms delay to run scripts cleanly
                 } else {
-                    log.debug("Skipping {} {} diary due to low difficulty", diary.getRight(), diary.getLeft());
+                    log.debug("Skipping {} {} diary due to low difficulty", diary.getDifficulty(), diary.getArea());
                 }
             } else {
                 // Karamja special case
-                log.info("Skipping {} {} diary start (not a completion with value {})", diary.getRight(), diary.getLeft(), value);
+                log.info("Skipping {} {} diary start (not a completion with value {})", diary.getDifficulty(), diary.getArea(), value);
             }
         }
     }
 
-    private void handle(String area, AchievementDiary.Difficulty difficulty) {
+    private void handle(AchievementDiary diary) {
         if (cooldownTicks.getAndSet(2) > 0) {
-            log.debug("Skipping diary completion during cooldown: {} {}", difficulty, area);
+            log.debug("Skipping diary completion during cooldown: {} {}", diary.getDifficulty(), diary.getArea());
             return;
         }
 
-        int total = getTotalCompleted();
+        client.runScript(DiaryNotifier.COMPLETED_TASKS_SCRIPT_ID);
+        int completedTasks = client.getIntStack()[0];
+        client.runScript(DiaryNotifier.TOTAL_TASKS_SCRIPT_ID);
+        int totalTasks = client.getIntStack()[0];
+
+        client.runScript(COMPLETED_AREA_TASKS_SCRIPT_ID, diary.getAreaId());
+        int completedAreaTasks = client.getIntStack()[0];
+        client.runScript(TOTAL_AREA_TASKS_SCRIPT_ID, diary.getAreaId());
+        int totalAreaTasks = client.getIntStack()[0];
+
+        int completedDiaries = getTotalCompleted();
         String player = Utils.getPlayerName(client);
         Template message = Template.builder()
             .template(config.diaryNotifyMessage())
             .replacementBoundary("%")
             .replacement("%USERNAME%", Replacements.ofText(player))
-            .replacement("%DIFFICULTY%", Replacements.ofText(difficulty.toString()))
-            .replacement("%AREA%", Replacements.ofWiki(area, area + " Diary"))
-            .replacement("%TOTAL%", Replacements.ofText(String.valueOf(total)))
+            .replacement("%DIFFICULTY%", Replacements.ofText(diary.getDifficulty().toString()))
+            .replacement("%AREA%", Replacements.ofWiki(diary.getArea(), diary.getArea() + " Diary"))
+            .replacement("%TOTAL%", Replacements.ofText(String.valueOf(completedDiaries)))
+            .replacement("%TASKS_COMPLETE%", Replacements.ofText(String.valueOf(completedTasks)))
+            .replacement("%TASKS_TOTAL%", Replacements.ofText(String.valueOf(totalTasks)))
+            .replacement("%AREA_TASKS_COMPLETE%", Replacements.ofText(String.valueOf(completedAreaTasks)))
+            .replacement("%AREA_TASKS_TOTAL%", Replacements.ofText(String.valueOf(totalAreaTasks)))
             .build();
 
         createMessage(config.diarySendImage(), NotificationBody.builder()
             .type(NotificationType.ACHIEVEMENT_DIARY)
             .text(message)
-            .extra(new DiaryNotificationData(area, difficulty, total))
+            .extra(new DiaryNotificationData(diary.getArea(), diary.getDifficulty(), completedDiaries, completedTasks, totalTasks, completedAreaTasks, totalAreaTasks))
             .playerName(player)
             .build());
     }
diff --git a/src/main/java/dinkplugin/notifiers/data/DiaryNotificationData.java b/src/main/java/dinkplugin/notifiers/data/DiaryNotificationData.java
index 821c18a..f953f2e 100644
--- a/src/main/java/dinkplugin/notifiers/data/DiaryNotificationData.java
+++ b/src/main/java/dinkplugin/notifiers/data/DiaryNotificationData.java
@@ -1,13 +1,68 @@
 package dinkplugin.notifiers.data;
 
 import dinkplugin.domain.AchievementDiary;
+import dinkplugin.message.Field;
 import lombok.EqualsAndHashCode;
 import lombok.Value;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @Value
 @EqualsAndHashCode(callSuper = false)
 public class DiaryNotificationData extends NotificationData {
+
+    /**
+     * The area of the diary that was just completed.
+     */
     String area;
+
+    /**
+     * The difficulty of the diary that was just completed.
+     */
     AchievementDiary.Difficulty difficulty;
+
+    /**
+     * The number of diaries that have been completed across all regions and difficulties.
+     */
     int total;
+
+    /**
+     * The number of diary tasks that have been completed across all regions and difficulties.
+     */
+    int tasksCompleted;
+
+    /**
+     * The total number of diary tasks within the game across all regions and difficulties.
+     */
+    int tasksTotal;
+
+    /**
+     * The number of diary tasks completed within this specific area.
+     */
+    int areaTasksCompleted;
+
+    /**
+     * The total number of diary tasks possible within this specific area.
+     */
+    int areaTasksTotal;
+
+    @Override
+    public List<Field> getFields() {
+        List<Field> fields = new ArrayList<>(2);
+
+        if (areaTasksCompleted > 0 && areaTasksTotal > 0) {
+            fields.add(
+                new Field(area + " Progress", Field.formatProgress(areaTasksCompleted, areaTasksTotal))
+            );
+        }
+
+        if (tasksCompleted > 0 && tasksTotal > 0) {
+            fields.add(
+                new Field("Overall Progress", Field.formatProgress(tasksCompleted, tasksTotal))
+            );
+        }
+
+        return fields;
+    }
 }


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java b/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java
index 870eb45..96e9388 100644
--- a/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/DiaryNotifierTest.java
@@ -27,6 +27,12 @@ import static org.mockito.Mockito.when;
 @SuppressWarnings("MagicConstant")
 class DiaryNotifierTest extends MockedNotifierTest {
 
+    private static final int COMPLETED_TASKS = 469;
+    private static final int TOTAL_TASKS = 492;
+
+    private static final int KARAMJA_TASKS_FROM_EASY_TO_HARD = 10 + 19 + 10;
+    private static final int KARAMJA_TOTAL_TASKS = KARAMJA_TASKS_FROM_EASY_TO_HARD + 5;
+
     @Bind
     @InjectMocks
     DiaryNotifier notifier;
@@ -44,18 +50,33 @@ class DiaryNotifierTest extends MockedNotifierTest {
 
         // init client mocks
         when(client.getGameState()).thenReturn(GameState.LOGGED_IN);
+        when(client.getIntStack()).thenReturn(
+            new int[] { COMPLETED_TASKS }, // first COMPLETED_TASKS_SCRIPT_ID is run
+            new int[] { TOTAL_TASKS }, // then TOTAL_TASKS_SCRIPT_ID is run
+            new int[] { KARAMJA_TASKS_FROM_EASY_TO_HARD }, // then COMPLETED_AREA_TASKS_SCRIPT_ID
+            new int[] { KARAMJA_TOTAL_TASKS } // then TOTAL_AREA_TASKS_SCRIPT_ID
+        );
     }
 
     @Test
     void testNotifyFirst() {
         // initially 0 diary completions
         when(client.getVarbitValue(anyInt())).thenReturn(0);
+        when(config.minDiaryDifficulty()).thenReturn(AchievementDiary.Difficulty.EASY);
 
         // perform enough ticks to trigger diary initialization
         IntStream.range(0, 16).forEach(i -> notifier.onTick());
 
         // trigger diary completion
-        int id = Varbits.DIARY_DESERT_ELITE;
+        int tasksCompleted = 11;
+        int totalAreaTasks = 11 + 12 + 10 + 6;
+        when(client.getIntStack()).thenReturn(
+            new int[] { tasksCompleted }, // first COMPLETED_TASKS_SCRIPT_ID is run
+            new int[] { TOTAL_TASKS }, // then TOTAL_TASKS_SCRIPT_ID is run
+            new int[] { tasksCompleted }, // then COMPLETED_AREA_TASKS_SCRIPT_ID is run
+            new int[] { totalAreaTasks } // then TOTAL_TASKS_SCRIPT_ID is run
+        );
+        int id = Varbits.DIARY_DESERT_EASY;
         plugin.onVarbitChanged(event(id, 1));
 
         // verify notification message
@@ -63,8 +84,8 @@ class DiaryNotifierTest extends MockedNotifierTest {
             PRIMARY_WEBHOOK_URL,
             false,
             NotificationBody.builder()
-                .text(buildTemplate(AchievementDiary.Difficulty.ELITE, "Desert", 1))
-                .extra(new DiaryNotificationData("Desert", AchievementDiary.Difficulty.ELITE, 1))
+                .text(buildTemplate(AchievementDiary.Difficulty.EASY, "Desert", 1))
+                .extra(new DiaryNotificationData("Desert", AchievementDiary.Difficulty.EASY, 1, tasksCompleted, TOTAL_TASKS, tasksCompleted, totalAreaTasks))
                 .type(NotificationType.ACHIEVEMENT_DIARY)
                 .playerName(PLAYER_NAME)
                 .build()
@@ -90,7 +111,7 @@ class DiaryNotifierTest extends MockedNotifierTest {
             false,
             NotificationBody.builder()
                 .text(buildTemplate(AchievementDiary.Difficulty.HARD, "Karamja", total + 1))
-                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1))
+                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1, COMPLETED_TASKS, TOTAL_TASKS, KARAMJA_TASKS_FROM_EASY_TO_HARD, KARAMJA_TOTAL_TASKS))
                 .type(NotificationType.ACHIEVEMENT_DIARY)
                 .playerName(PLAYER_NAME)
                 .build()
@@ -115,7 +136,7 @@ class DiaryNotifierTest extends MockedNotifierTest {
             false,
             NotificationBody.builder()
                 .text(buildTemplate(AchievementDiary.Difficulty.HARD, "Karamja", total + 1))
-                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1))
+                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1, COMPLETED_TASKS, TOTAL_TASKS, KARAMJA_TASKS_FROM_EASY_TO_HARD, KARAMJA_TOTAL_TASKS))
                 .type(NotificationType.ACHIEVEMENT_DIARY)
                 .playerName(PLAYER_NAME)
                 .build()
@@ -133,6 +154,14 @@ class DiaryNotifierTest extends MockedNotifierTest {
         IntStream.range(0, 16).forEach(i -> notifier.onTick());
 
         // trigger diary completion
+        int westernTasksFromEasyToHard = 11 + 13 + 13;
+        int westernTasksTotal = westernTasksFromEasyToHard + 7;
+        when(client.getIntStack()).thenReturn(
+            new int[] { COMPLETED_TASKS }, // first COMPLETED_TASKS_SCRIPT_ID is run
+            new int[] { TOTAL_TASKS }, // then TOTAL_TASKS_SCRIPT_ID is run
+            new int[] { westernTasksFromEasyToHard }, // then COMPLETED_AREA_TASKS_SCRIPT_ID
+            new int[] { westernTasksTotal } // then TOTAL_AREA_TASKS_SCRIPT_ID
+        );
         notifier.onMessageBox("Congratulations! You have completed all of the hard tasks in the Western Province area. Speak to the Elder Gnome child at the Gnome Stronghold to claim your reward.");
 
         // verify notification message
@@ -141,7 +170,7 @@ class DiaryNotifierTest extends MockedNotifierTest {
             false,
             NotificationBody.builder()
                 .text(buildTemplate(AchievementDiary.Difficulty.HARD, "Western Provinces", total + 1))
-                .extra(new DiaryNotificationData("Western Provinces", AchievementDiary.Difficulty.HARD, total + 1))
+                .extra(new DiaryNotificationData("Western Provinces", AchievementDiary.Difficulty.HARD, total + 1, COMPLETED_TASKS, TOTAL_TASKS, westernTasksFromEasyToHard, westernTasksTotal))
                 .type(NotificationType.ACHIEVEMENT_DIARY)
                 .playerName(PLAYER_NAME)
                 .build()
@@ -167,7 +196,7 @@ class DiaryNotifierTest extends MockedNotifierTest {
             false,
             NotificationBody.builder()
                 .text(buildTemplate(AchievementDiary.Difficulty.HARD, "Karamja", total + 1))
-                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1))
+                .extra(new DiaryNotificationData("Karamja", AchievementDiary.Difficulty.HARD, total + 1, COMPLETED_TASKS, TOTAL_TASKS, KARAMJA_TASKS_FROM_EASY_TO_HARD, KARAMJA_TOTAL_TASKS))
                 .type(NotificationType.ACHIEVEMENT_DIARY)
                 .playerName(PLAYER_NAME)
                 .build()