Skip to content

Commit

Permalink
feat: add raid party members to loot metadata (#478)
Browse files Browse the repository at this point in the history
  • Loading branch information
iProdigy authored Aug 5, 2024
1 parent d84e043 commit 79c9ce5
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Unreleased

- Dev: Add raid party members to loot notification metadata. (#478)

## 1.10.4

- Bugfix: Fire death and loot notifications when 10 or more item icons were embedded. (#509)
Expand Down
11 changes: 8 additions & 3 deletions docs/json-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,21 +233,26 @@ JSON for Loot Notifications:
"rarity": null
}
],
"source": "Giant rat",
"category": "NPC",
"source": "Tombs of Amascut",
"party": ["%USERNAME%", "another RSN", "yet another RSN"],
"category": "EVENT",
"killCount": 60,
"rarestProbability": 0.001
},
"type": "LOOT"
}
```
`killCount` is only specified for NPC loot with the base RuneLite Loot Tracker plugin enabled.
The possible values for `extra.category` correspond to the [`LootRecordType`](https://github.com/runelite/api.runelite.net/blob/master/http-api/src/main/java/net/runelite/http/api/loottracker/LootRecordType.java) enum.
`killCount` is only specified for NPC/EVENT loot with the base RuneLite Loot Tracker plugin enabled.
`rarity` is currently only populated for NPC drops. This data is (imperfectly) scraped from the wiki, so it may not be 100% accurate. Also, we do not report a rarity if the NPC always drops the item on every kill.
The items are valued at GE prices (when possible) if the user has not disabled the `Use actively traded price` base RuneLite setting. Otherwise, the store price of the item is used.
The `extra.party` field is only populated for raids loot (i.e., COX, TOA, TOB).
### Slayer
JSON for Slayer Notifications:
Expand Down
19 changes: 18 additions & 1 deletion src/main/java/dinkplugin/notifiers/LootNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import net.runelite.client.util.QuantityFormatter;
import net.runelite.http.api.loottracker.LootRecordType;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.inject.Inject;
import javax.inject.Singleton;
Expand Down Expand Up @@ -229,6 +231,7 @@ private void handleNotify(Collection<ItemStack> items, String dropper, LootRecor
SerializedItemStack keyItem = rarest != null ? rarest : max;
Double rarity = rarest != null ? rarest.getRarity() : null;
boolean screenshot = config.lootSendImage() && totalStackValue >= config.lootImageMinValue();
Collection<String> party = type == LootRecordType.EVENT ? getParty(dropper) : null;
Evaluable source = type == LootRecordType.PLAYER
? Replacements.ofLink(dropper, config.playerLookupService().getPlayerUrl(dropper))
: Replacements.ofWiki(dropper);
Expand All @@ -244,14 +247,28 @@ private void handleNotify(Collection<ItemStack> items, String dropper, LootRecor
NotificationBody.builder()
.text(notifyMessage)
.embeds(embeds)
.extra(new LootNotificationData(serializedItems, dropper, type, kc, rarity))
.extra(new LootNotificationData(serializedItems, dropper, type, kc, rarity, party))
.type(NotificationType.LOOT)
.thumbnailUrl(ItemUtils.getItemImageUrl(keyItem.getId()))
.build()
);
}
}

@Nullable
private Collection<String> getParty(@NotNull String source) {
switch (source) {
case "Chambers of Xeric":
return Utils.getXericChambersParty(client);
case "Tombs of Amascut":
return Utils.getAmascutTombsParty(client);
case "Theatre of Blood":
return Utils.getBloodTheatreParty(client);
default:
return null;
}
}

private static boolean matches(Collection<Pattern> regexps, String input) {
for (Pattern regex : regexps) {
if (regex.matcher(input).find())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ public class LootNotificationData extends NotificationData {
@Nullable
Double rarestProbability;

@Nullable
Collection<String> party;

@Override
public List<Field> getFields() {
List<Field> fields = new ArrayList<>(3);
List<Field> fields = new ArrayList<>(4);
if (killCount != null) {
fields.add(
new Field(
Expand All @@ -45,6 +48,9 @@ public List<Field> getFields() {
if (rarestProbability != null) {
fields.add(new Field("Item Rarity", Field.formatProbability(rarestProbability)));
}
if (party != null && !party.isEmpty()) {
fields.add(new Field("Party Size", Field.formatBlock("", String.valueOf(party.size()))));
}
return fields;
}

Expand Down
46 changes: 46 additions & 0 deletions src/main/java/dinkplugin/util/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import net.runelite.api.Client;
import net.runelite.api.Varbits;
import net.runelite.api.annotations.Component;
import net.runelite.api.annotations.VarCStr;
import net.runelite.api.widgets.InterfaceID;
import net.runelite.api.widgets.Widget;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.util.ColorUtil;
Expand Down Expand Up @@ -41,7 +43,11 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.regex.Pattern;
Expand All @@ -58,6 +64,10 @@ public class Utils {

private final char ELLIPSIS = '\u2026'; // '…'

@SuppressWarnings("MagicConstant")
private final @VarCStr int TOA_MEMBER_NAME = 1099, TOB_MEMBER_NAME = 330;
private final int TOA_PARTY_MAX_SIZE = 8, TOB_PARTY_MAX_SIZE = 5;

/**
* Custom padding for applying SHA-256 to a long.
* SHA-256 adds '0' padding bits such that L+1+K+64 mod 512 == 0.
Expand Down Expand Up @@ -193,6 +203,42 @@ public String getChatBadge(@NotNull AccountType type, boolean seasonal) {
}
}

public Collection<String> getXericChambersParty(@NotNull Client client) {
Widget widget = client.getWidget(InterfaceID.RAIDING_PARTY, 10);
if (widget == null) return Collections.emptyList();

Widget[] children = widget.getChildren();
if (children == null) return Collections.emptyList();

List<String> names = new ArrayList<>(children.length);
for (Widget child : children) {
String text = child.getText();
if (text.startsWith("<col=ffffff>")) {
names.add(sanitize(text));
}
}
return names;
}

public Collection<String> getAmascutTombsParty(@NotNull Client client) {
return getVarcStrings(client, TOA_MEMBER_NAME, TOA_PARTY_MAX_SIZE);
}

public Collection<String> getBloodTheatreParty(@NotNull Client client) {
return getVarcStrings(client, TOB_MEMBER_NAME, TOB_PARTY_MAX_SIZE);
}

private List<String> getVarcStrings(@NotNull Client client, @VarCStr final int initialVarcId, final int maxSize) {
List<String> strings = new ArrayList<>(maxSize);
for (int i = 0; i < maxSize; i++) {
// noinspection MagicConstant
String name = client.getVarcStrValue(initialVarcId + i);
if (name == null || name.isEmpty()) continue;
strings.add(name.replace('\u00A0', ' '));
}
return strings;
}

public static boolean hideWidget(boolean shouldHide, Client client, @Component int info) {
if (!shouldHide)
return false;
Expand Down
36 changes: 18 additions & 18 deletions src/test/java/dinkplugin/notifiers/LootNotifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ void testNotifyNpc() {
.replacement("{{source}}", Replacements.ofWiki(name))
.build()
)
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), name, LootRecordType.NPC, kc + 1, null))
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), name, LootRecordType.NPC, kc + 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -142,7 +142,7 @@ void testNotifyNpcRarity() {
.replacement("{{source}}", Replacements.ofWiki(name))
.build()
)
.extra(new LootNotificationData(List.of(new RareItemStack(ItemID.LARRANS_KEY, 1, LARRAN_PRICE, "Larran's key", rarity)), name, LootRecordType.NPC, 1, rarity))
.extra(new LootNotificationData(List.of(new RareItemStack(ItemID.LARRANS_KEY, 1, LARRAN_PRICE, "Larran's key", rarity)), name, LootRecordType.NPC, 1, rarity, null))
.type(NotificationType.LOOT)
.thumbnailUrl(ItemUtils.getItemImageUrl(ItemID.LARRANS_KEY))
.build()
Expand Down Expand Up @@ -215,7 +215,7 @@ void testNotifyAllowlist() {
.replacement("{{source}}", Replacements.ofLink(LOOTED_NAME, config.playerLookupService().getPlayerUrl(LOOTED_NAME)))
.build()
)
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null))
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -244,7 +244,7 @@ void testNotifyAllowlistWildcard() {
.replacement("{{source}}", Replacements.ofLink(LOOTED_NAME, config.playerLookupService().getPlayerUrl(LOOTED_NAME)))
.build()
)
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null))
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -293,7 +293,7 @@ void testNotifyWhisperer() {
.replacement("{{source}}", Replacements.ofWiki(name))
.build()
)
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), name, LootRecordType.NPC, 1, null))
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), name, LootRecordType.NPC, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -348,7 +348,7 @@ void testNotifyPickpocket() {
.replacement("{{source}}", Replacements.ofWiki(LOOTED_NAME))
.build()
)
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), LOOTED_NAME, LootRecordType.PICKPOCKET, 1, null))
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), LOOTED_NAME, LootRecordType.PICKPOCKET, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -386,7 +386,7 @@ void testNotifyClue() {
.replacement("{{source}}", Replacements.ofWiki(source))
.build()
)
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, 42, null))
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, 42, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -429,7 +429,7 @@ void testNotifyPlayer() {
.replacement("{{source}}", Replacements.ofLink(LOOTED_NAME, config.playerLookupService().getPlayerUrl(LOOTED_NAME)))
.build()
)
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null))
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -460,7 +460,7 @@ void testNotifyPlayerForwarded() {
.replacement("{{source}}", Replacements.ofLink(LOOTED_NAME, config.playerLookupService().getPlayerUrl(LOOTED_NAME)))
.build()
)
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null))
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -490,7 +490,7 @@ void testNotifyPlayerForwardBlank() {
.replacement("{{source}}", Replacements.ofLink(LOOTED_NAME, config.playerLookupService().getPlayerUrl(LOOTED_NAME)))
.build()
)
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null))
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -522,7 +522,7 @@ void testNotifyPkChest() {
.replacement("{{source}}", Replacements.ofWiki(source))
.build()
)
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.OPAL, 1, OPAL_PRICE, "Opal"), new SerializedItemStack(ItemID.TUNA, 2, TUNA_PRICE, "Tuna")), source, LootRecordType.EVENT, 1, null))
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.OPAL, 1, OPAL_PRICE, "Opal"), new SerializedItemStack(ItemID.TUNA, 2, TUNA_PRICE, "Tuna")), source, LootRecordType.EVENT, 1, null, null))
.type(NotificationType.LOOT)
.thumbnailUrl(ItemUtils.getItemImageUrl(ItemID.TUNA))
.build()
Expand Down Expand Up @@ -591,7 +591,7 @@ void testNotifyMultiple() {
.replacement("{{source}}", Replacements.ofWiki(LOOTED_NAME))
.build()
)
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.OPAL, 1, OPAL_PRICE, "Opal"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.EVENT, 1, null))
.extra(new LootNotificationData(Arrays.asList(new SerializedItemStack(ItemID.RUBY, 1, RUBY_PRICE, "Ruby"), new SerializedItemStack(ItemID.OPAL, 1, OPAL_PRICE, "Opal"), new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.EVENT, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -628,7 +628,7 @@ void testNotifyRepeated() {
.replacement("{{source}}", Replacements.ofWiki(LOOTED_NAME))
.build()
)
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.TUNA, 5, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.EVENT, 1, null))
.extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.TUNA, 5, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.EVENT, 1, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -680,7 +680,7 @@ void testNotifyGauntlet() {
.replacement("{{source}}", Replacements.ofWiki(source))
.build()
)
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, kc, null))
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, kc, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -712,7 +712,7 @@ void testNotifyCorruptedGauntlet() {
.replacement("{{source}}", Replacements.ofWiki(realSource))
.build()
)
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), realSource, LootRecordType.EVENT, kc, null))
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), realSource, LootRecordType.EVENT, kc, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -743,7 +743,7 @@ void testNotifyAmascut() {
.replacement("{{source}}", Replacements.ofWiki(source))
.build()
)
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, kc, null))
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, kc, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -774,7 +774,7 @@ void testNotifyAmascutExpert() {
.replacement("{{source}}", Replacements.ofWiki(source))
.build()
)
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, kc, null))
.extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.RUBY, quantity, RUBY_PRICE, "Ruby")), source, LootRecordType.EVENT, kc, null, null))
.type(NotificationType.LOOT)
.build()
);
Expand Down Expand Up @@ -810,7 +810,7 @@ void testNotifyRarityValueIntersectionValue() {
.replacement("{{source}}", Replacements.ofWiki(name))
.build()
)
.extra(new LootNotificationData(List.of(new RareItemStack(ItemID.LARRANS_KEY, 1, LARRAN_PRICE, "Larran's key", rarity)), name, LootRecordType.NPC, 1, rarity))
.extra(new LootNotificationData(List.of(new RareItemStack(ItemID.LARRANS_KEY, 1, LARRAN_PRICE, "Larran's key", rarity)), name, LootRecordType.NPC, 1, rarity, null))
.type(NotificationType.LOOT)
.thumbnailUrl(ItemUtils.getItemImageUrl(ItemID.LARRANS_KEY))
.build()
Expand Down

0 comments on commit 79c9ce5

Please sign in to comment.