Repository: pajlads/DinkPlugin (java)

Issue:
Add Total Collection Logs to Collection Log Notification
Add an additional checkbox in the Collection Log menu for "Total Collection Logs". Having this check will also post the player's total collection log slots in the notification.

Example of Discord notification:
User has added a new item to their collection log: Trident of the seas (full) (701/1443)

Example of the message in-game:
![image](https://user-images.githubusercontent.com/87504521/220497959-50e97f69-fd57-4903-8931-7fdbd7b4f9e6.png)


Pull request:
feat(collection): include number of completed entries
Closes #173

![image](https://user-images.githubusercontent.com/8106344/220544821-9d091375-21c6-4fff-9104-36ac1739a418.png)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a237b56..f16dc9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 ## Unreleased
 
+- Minor: Include number of completed entries in collection log notification. (#174)
+
 ## 1.3.1
 
 - Minor: Include pet acquisition milestone in webhook metadata from clan message. (#169)
diff --git a/README.md b/README.md
index b76d6e8..00d4148 100644
--- a/README.md
+++ b/README.md
@@ -180,7 +180,9 @@ The examples below omit `embeds` and `playerName` keys because they are always t
   "extra": {
     "itemName": "Zamorak chaps",
     "itemId": 10372,
-    "price": 500812
+    "price": 500812,
+    "completedEntries": 420,
+    "totalEntries": 1443
   },
   "type": "COLLECTION"
 }
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index 3272825..cd03795 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -86,6 +86,7 @@ public class DinkPlugin extends Plugin {
     }
 
     void resetNotifiers() {
+        collectionNotifier.reset();
         petNotifier.reset();
         clueNotifier.reset();
         diaryNotifier.reset();
@@ -127,6 +128,7 @@ public class DinkPlugin extends Plugin {
 
     @Subscribe
     public void onGameTick(GameTick event) {
+        collectionNotifier.onTick();
         petNotifier.onTick();
         clueNotifier.onTick();
         slayerNotifier.onTick();
@@ -197,6 +199,7 @@ public class DinkPlugin extends Plugin {
     @Subscribe
     public void onVarbitChanged(VarbitChanged event) {
         settingsManager.onVarbitChanged(event);
+        collectionNotifier.onVarPlayer(event);
         diaryNotifier.onVarbitChanged(event);
     }
 
diff --git a/src/main/java/dinkplugin/notifiers/CollectionNotifier.java b/src/main/java/dinkplugin/notifiers/CollectionNotifier.java
index c41bd6e..8c36f1a 100644
--- a/src/main/java/dinkplugin/notifiers/CollectionNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/CollectionNotifier.java
@@ -6,17 +6,35 @@ import dinkplugin.util.ItemSearcher;
 import dinkplugin.util.ItemUtils;
 import dinkplugin.util.Utils;
 import dinkplugin.notifiers.data.CollectionNotificationData;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.Client;
+import net.runelite.api.GameState;
+import net.runelite.api.events.VarbitChanged;
 import net.runelite.client.game.ItemManager;
 import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.VisibleForTesting;
 
 import javax.inject.Inject;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+@Slf4j
 public class CollectionNotifier extends BaseNotifier {
     static final Pattern COLLECTION_LOG_REGEX = Pattern.compile("New item added to your collection log: (?<itemName>(.*))");
     public static final String ADDITION_WARNING = "Collection notifier will not fire unless you enable the game setting: Collection log - New addition notification";
 
+    @VisibleForTesting
+    static final int COMPLETED_VARP = 2943, TOTAL_VARP = 2944; // https://github.com/Joshua-F/cs2-scripts/blob/master/scripts/[clientscript,collection_init_frame].cs2#L3
+
+    /**
+     * The number of completed entries in the collection log, as implied by {@link #COMPLETED_VARP}.
+     */
+    private final AtomicInteger completed = new AtomicInteger(-1);
+
+    @Inject
+    private Client client;
+
     @Inject
     private ItemManager itemManager;
 
@@ -33,6 +51,38 @@ public class CollectionNotifier extends BaseNotifier {
         return config.collectionWebhook();
     }
 
+    public void reset() {
+        // note: unlike other notifiers, we do not need to reset completed after each message
+        // in fact, resetting would be problematic for an edge case with multiple completions in a single tick
+        this.completed.set(-1);
+    }
+
+    public void onTick() {
+        if (client.getGameState() != GameState.LOGGED_IN) {
+            // indicate that the latest completion count should be updated
+            completed.set(-1);
+        } else if (completed.get() < 0) {
+            // initialize collection log entry completion count
+            completed.set(client.getVarpValue(COMPLETED_VARP));
+        }
+    }
+
+    public void onVarPlayer(VarbitChanged event) {
+        if (event.getVarpId() != COMPLETED_VARP)
+            return;
+
+        // Currently, this varp is sent early enough to be read on the first logged-in tick.
+        // For robustness, we also allow initialization here just in case the varp is sent with greater delay.
+
+        // Note: upon a completion, this varp is not updated until a few ticks after the collection log message.
+        // However, this behavior could also change, which is why here we don't synchronize "completed" beyond initialization.
+
+        int old = completed.get();
+        if (old <= 0) {
+            completed.compareAndSet(old, event.getValue());
+        }
+    }
+
     public void onChatMessage(String chatMessage) {
         if (!isEnabled()) return;
 
@@ -49,12 +99,31 @@ public class CollectionNotifier extends BaseNotifier {
             new String[] { Utils.getPlayerName(client), itemName }
         );
 
+        // varp isn't updated for a few ticks, so we increment the count locally.
+        // this approach also has the benefit of yielding incrementing values even when
+        // multiple collection log entries are completed within a single tick.
+        int completed = this.completed.incrementAndGet();
+        int total = client.getVarpValue(TOTAL_VARP); // unique; doesn't over-count duplicates
+        boolean varpValid = total > 0 && completed > 0;
+        if (!varpValid) {
+            // This should never occur unless Jagex changes the var player id's
+            log.warn("Collection log completion VarPlayer was invalid ({} / {})! Please report to Dink issue tracker", completed, total);
+        }
+
         Integer itemId = itemSearcher.findItemId(itemName);
         Long price = itemId != null ? ItemUtils.getPrice(itemManager, itemId) : null;
+        CollectionNotificationData extra = new CollectionNotificationData(
+            itemName,
+            itemId,
+            price,
+            varpValid ? completed : null,
+            varpValid ? total : null
+        );
+
         createMessage(config.collectionSendImage(), NotificationBody.builder()
             .text(notifyMessage)
             .thumbnailUrl(itemId != null ? ItemUtils.getItemImageUrl(itemId) : null)
-            .extra(new CollectionNotificationData(itemName, itemId, price))
+            .extra(extra)
             .type(NotificationType.COLLECTION)
             .build());
     }
diff --git a/src/main/java/dinkplugin/notifiers/data/CollectionNotificationData.java b/src/main/java/dinkplugin/notifiers/data/CollectionNotificationData.java
index 6abeb93..9817527 100644
--- a/src/main/java/dinkplugin/notifiers/data/CollectionNotificationData.java
+++ b/src/main/java/dinkplugin/notifiers/data/CollectionNotificationData.java
@@ -1,10 +1,14 @@
 package dinkplugin.notifiers.data;
 
+import dinkplugin.message.Field;
 import lombok.EqualsAndHashCode;
 import lombok.Value;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.Collections;
+import java.util.List;
+
 @Value
 @EqualsAndHashCode(callSuper = false)
 public class CollectionNotificationData extends NotificationData {
@@ -18,4 +22,23 @@ public class CollectionNotificationData extends NotificationData {
     @Nullable
     Long price;
 
+    @Nullable
+    Integer completedEntries;
+
+    @Nullable
+    Integer totalEntries;
+
+    @Override
+    public List<Field> getFields() {
+        if (completedEntries != null && totalEntries != null) {
+            double percent = 100.0 * completedEntries / totalEntries;
+            return Collections.singletonList(
+                new Field(
+                    "Completed Entries",
+                    Field.formatBlock("", String.format("%d/%d (%.1f%%)", completedEntries, totalEntries, percent)))
+            );
+        }
+
+        return super.getFields();
+    }
 }


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/CollectionNotifierTest.java b/src/test/java/dinkplugin/notifiers/CollectionNotifierTest.java
index 02d76dd..5a5555d 100644
--- a/src/test/java/dinkplugin/notifiers/CollectionNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/CollectionNotifierTest.java
@@ -5,6 +5,7 @@ import dinkplugin.message.NotificationBody;
 import dinkplugin.message.NotificationType;
 import dinkplugin.notifiers.data.CollectionNotificationData;
 import dinkplugin.util.ItemSearcher;
+import net.runelite.api.GameState;
 import net.runelite.api.ItemID;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -19,6 +20,8 @@ import static org.mockito.Mockito.when;
 
 class CollectionNotifierTest extends MockedNotifierTest {
 
+    private static final int TOTAL_ENTRIES = 1443;
+
     @Bind
     @InjectMocks
     CollectionNotifier notifier;
@@ -36,6 +39,12 @@ class CollectionNotifierTest extends MockedNotifierTest {
         when(config.notifyCollectionLog()).thenReturn(true);
         when(config.collectionSendImage()).thenReturn(false);
         when(config.collectionNotifyMessage()).thenReturn("%USERNAME% has added %ITEM% to their collection");
+
+        // init client mocks
+        when(client.getVarpValue(CollectionNotifier.COMPLETED_VARP)).thenReturn(0);
+        when(client.getVarpValue(CollectionNotifier.TOTAL_VARP)).thenReturn(TOTAL_ENTRIES);
+        when(client.getGameState()).thenReturn(GameState.LOGGED_IN);
+        notifier.onTick();
     }
 
     @Test
@@ -54,7 +63,7 @@ class CollectionNotifierTest extends MockedNotifierTest {
             false,
             NotificationBody.builder()
                 .text(String.format("%s has added %s to their collection", PLAYER_NAME, item))
-                .extra(new CollectionNotificationData(item, ItemID.SEERCULL, (long) price))
+                .extra(new CollectionNotificationData(item, ItemID.SEERCULL, (long) price, 1, TOTAL_ENTRIES))
                 .type(NotificationType.COLLECTION)
                 .build()
         );