From 3643585c72b58b174d6c16a297c260c34fdde4b3 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 20:53:26 -0800 Subject: [PATCH 01/21] feat: add leagues notifier for areas, relics, and tasks --- src/main/java/dinkplugin/DinkPlugin.java | 3 + .../java/dinkplugin/DinkPluginConfig.java | 89 +++++- .../dinkplugin/domain/LeagueRelicTier.java | 38 +++ .../domain/LeagueTaskDifficulty.java | 37 +++ .../message/DiscordMessageHandler.java | 4 +- .../dinkplugin/message/NotificationType.java | 3 + .../dinkplugin/notifiers/LeaguesNotifier.java | 271 ++++++++++++++++++ .../data/LeaguesAreaNotificationData.java | 31 ++ .../data/LeaguesRelicNotificationData.java | 37 +++ .../data/LeaguesTaskNotificationData.java | 48 ++++ src/main/java/dinkplugin/util/Utils.java | 7 +- 11 files changed, 563 insertions(+), 5 deletions(-) create mode 100644 src/main/java/dinkplugin/domain/LeagueRelicTier.java create mode 100644 src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java create mode 100644 src/main/java/dinkplugin/notifiers/LeaguesNotifier.java create mode 100644 src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java create mode 100644 src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java create mode 100644 src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java index 93aacbef..4b1792b4 100644 --- a/src/main/java/dinkplugin/DinkPlugin.java +++ b/src/main/java/dinkplugin/DinkPlugin.java @@ -10,6 +10,7 @@ import dinkplugin.notifiers.GrandExchangeNotifier; import dinkplugin.notifiers.GroupStorageNotifier; import dinkplugin.notifiers.KillCountNotifier; +import dinkplugin.notifiers.LeaguesNotifier; import dinkplugin.notifiers.LevelNotifier; import dinkplugin.notifiers.MetaNotifier; import dinkplugin.notifiers.LootNotifier; @@ -82,6 +83,7 @@ public class DinkPlugin extends Plugin { private @Inject PlayerKillNotifier pkNotifier; private @Inject GroupStorageNotifier groupStorageNotifier; private @Inject GrandExchangeNotifier grandExchangeNotifier; + private @Inject LeaguesNotifier leaguesNotifier; private @Inject MetaNotifier metaNotifier; @Override @@ -186,6 +188,7 @@ public void onChatMessage(ChatMessage message) { combatTaskNotifier.onGameMessage(chatMessage); deathNotifier.onGameMessage(chatMessage); speedrunNotifier.onGameMessage(chatMessage); + leaguesNotifier.onGameMessage(chatMessage); break; case FRIENDSCHATNOTIFICATION: diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java index 272b2051..e1763a3f 100644 --- a/src/main/java/dinkplugin/DinkPluginConfig.java +++ b/src/main/java/dinkplugin/DinkPluginConfig.java @@ -4,6 +4,7 @@ import dinkplugin.domain.ClueTier; import dinkplugin.domain.CombatAchievementTier; import dinkplugin.domain.FilterMode; +import dinkplugin.domain.LeagueTaskDifficulty; import dinkplugin.domain.PlayerLookupService; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; @@ -151,6 +152,14 @@ public interface DinkPluginConfig extends Config { ) String grandExchangeSection = "Grand Exchange"; + @ConfigSection( + name = "Leagues", + description = "Settings for notifying when you complete league tasks, unlock areas, and redeem relics", + position = 200, + closedByDefault = true + ) + String leaguesSection = "Leagues"; + @ConfigSection( name = "Advanced", description = "Do not modify without fully understanding these settings", @@ -351,7 +360,8 @@ default String metadataWebhook() { @ConfigItem( keyName = "ignoreSeasonalWorlds", name = "Ignore Seasonal Worlds", - description = "Whether to suppress notifications that occur on seasonal worlds like Leagues", + description = "Whether to suppress notifications that occur on seasonal worlds like Leagues.
" + + "Note: the Leagues-specific notifier uses an independent config toggle", position = 1015, section = advancedSection ) @@ -546,6 +556,17 @@ default String grandExchangeWebhook() { return ""; } + @ConfigItem( + keyName = "leaguesWebhook", + name = "Leagues Webhook Override", + description = "If non-empty, Leagues messages are sent to this URL, instead of the primary URL", + position = -1, + section = webhookSection + ) + default String leaguesWebhook() { + return ""; + } + @ConfigItem( keyName = "collectionLogEnabled", name = "Enable collection log", @@ -1660,4 +1681,70 @@ default String grandExchangeNotifyMessage() { return "%USERNAME% %TYPE% %ITEM% on the GE"; } + @ConfigItem( + keyName = "notifyLeagues", + name = "Enable Leagues", + description = "Enable notifications upon various leagues events", + position = 200, + section = leaguesSection + ) + default boolean notifyLeagues() { + return false; + } + + @ConfigItem( + keyName = "leaguesSendImage", + name = "Send Image", + description = "Send image with the notification", + position = 201, + section = leaguesSection + ) + default boolean leaguesSendImage() { + return true; + } + + @ConfigItem( + keyName = "leaguesAreaUnlock", + name = "Send Area Unlocks", + description = "Send notifications upon area unlocks", + position = 202, + section = leaguesSection + ) + default boolean leaguesAreaUnlock() { + return true; + } + + @ConfigItem( + keyName = "leaguesRelicUnlock", + name = "Send Relic Unlocks", + description = "Send notifications upon relic unlocks", + position = 203, + section = leaguesSection + ) + default boolean leaguesRelicUnlock() { + return true; + } + + @ConfigItem( + keyName = "leaguesTaskCompletion", + name = "Send Completed Tasks", + description = "Send notifications upon completing a task", + position = 204, + section = leaguesSection + ) + default boolean leaguesTaskCompletion() { + return true; + } + + @ConfigItem( + keyName = "leaguesTaskMinTier", + name = "Task Min Difficulty", + description = "The minimum tier of a task for a notification to be sent", + position = 205, + section = leaguesSection + ) + default LeagueTaskDifficulty leaguesTaskMinTier() { + return LeagueTaskDifficulty.HARD; + } + } diff --git a/src/main/java/dinkplugin/domain/LeagueRelicTier.java b/src/main/java/dinkplugin/domain/LeagueRelicTier.java new file mode 100644 index 00000000..1ae9d4b4 --- /dev/null +++ b/src/main/java/dinkplugin/domain/LeagueRelicTier.java @@ -0,0 +1,38 @@ +package dinkplugin.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.NavigableMap; +import java.util.TreeMap; + +@Getter +@RequiredArgsConstructor +public enum LeagueRelicTier { + ONE(0), + TWO(500), + THREE(1_200), + FOUR(2_000), + FIVE(4_000), + SIX(7_500), + SEVEN(15_000), + EIGHT(24_000); + + /** + * Points required to unlock a relic of a given tier. + * + * @see Wiki Reference + */ + private final int points; + + public static final NavigableMap TIER_BY_POINTS; + + static { + NavigableMap tiers = new TreeMap<>(); + for (LeagueRelicTier tier : values()) { + tiers.put(tier.getPoints(), tier); + } + TIER_BY_POINTS = Collections.unmodifiableNavigableMap(tiers); + } +} diff --git a/src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java b/src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java new file mode 100644 index 00000000..73d73215 --- /dev/null +++ b/src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java @@ -0,0 +1,37 @@ +package dinkplugin.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +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 LeagueTaskDifficulty { + EASY(10), + MEDIUM(40), + HARD(80), + ELITE(200), + MASTER(400); + + /** + * Points earned from completed a task of the given difficulty. + * + * @see Wiki Reference + */ + private final int points; + private final String displayName = this.name().charAt(0) + this.name().substring(1).toLowerCase(); + + @Override + public String toString() { + return this.displayName; + } + + public static final Map TIER_BY_LOWER_NAME = Collections.unmodifiableMap( + Arrays.stream(values()).collect(Collectors.toMap(t -> t.name().toLowerCase(), Function.identity())) + ); +} diff --git a/src/main/java/dinkplugin/message/DiscordMessageHandler.java b/src/main/java/dinkplugin/message/DiscordMessageHandler.java index d2d029bd..2a33b38c 100644 --- a/src/main/java/dinkplugin/message/DiscordMessageHandler.java +++ b/src/main/java/dinkplugin/message/DiscordMessageHandler.java @@ -221,7 +221,7 @@ private NotificationBody enrichBody(NotificationBody mBody, boolean sendIm NotificationBody.NotificationBodyBuilder builder = mBody.toBuilder(); - if (!config.ignoreSeasonal()) { + if (!config.ignoreSeasonal() && !mBody.isSeasonalWorld()) { builder.seasonalWorld(client.getWorldType().contains(WorldType.SEASONAL)); } @@ -333,7 +333,7 @@ private static List computeEmbeds(@NotNull NotificationBody body, bool Author author = Author.builder() .name(body.getPlayerName()) .url(playerLookupService.getPlayerUrl(body.getPlayerName())) - .iconUrl(Utils.getChatBadge(body.getAccountType())) + .iconUrl(Utils.getChatBadge(body.getAccountType(), body.isSeasonalWorld())) .build(); Footer footer = StringUtils.isBlank(footerText) ? null : Footer.builder() .text(Utils.truncate(footerText, Embed.MAX_FOOTER_LENGTH)) diff --git a/src/main/java/dinkplugin/message/NotificationType.java b/src/main/java/dinkplugin/message/NotificationType.java index dfd949ad..67319369 100644 --- a/src/main/java/dinkplugin/message/NotificationType.java +++ b/src/main/java/dinkplugin/message/NotificationType.java @@ -24,6 +24,9 @@ public enum NotificationType { PLAYER_KILL("Player Kill", "playerKillImage.png", WIKI_IMG_BASE_URL + "Skull_(status)_icon.png"), GROUP_STORAGE("Group Shared Storage", "groupStorage.png", WIKI_IMG_BASE_URL + "Coins_10000.png"), GRAND_EXCHANGE("Grand Exchange", "grandExchange.png", WIKI_IMG_BASE_URL + "Grand_Exchange_icon.png"), + LEAGUES_AREA("Area Unlocked", "leaguesArea.png", WIKI_IMG_BASE_URL + "Trailblazer_Reloaded_League_-_%3F_Relic.png"), + LEAGUES_RELIC("Relic Chosen", "leaguesRelic.png", WIKI_IMG_BASE_URL + "Trailblazer_Reloaded_League_-_relics_icon.png"), + LEAGUES_TASK("Task Completed", "leaguesTask.png", WIKI_IMG_BASE_URL + "Trailblazer_Reloaded_League_icon.png"), LOGIN("Player Login", "login.png", WIKI_IMG_BASE_URL + "Prop_sword.png"); private final String title; diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java new file mode 100644 index 00000000..e4397618 --- /dev/null +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -0,0 +1,271 @@ +package dinkplugin.notifiers; + +import dinkplugin.domain.LeagueRelicTier; +import dinkplugin.domain.LeagueTaskDifficulty; +import dinkplugin.message.NotificationBody; +import dinkplugin.message.NotificationType; +import dinkplugin.message.templating.Replacements; +import dinkplugin.message.templating.Template; +import dinkplugin.notifiers.data.LeaguesAreaNotificationData; +import dinkplugin.notifiers.data.LeaguesRelicNotificationData; +import dinkplugin.notifiers.data.LeaguesTaskNotificationData; +import dinkplugin.util.Utils; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.WorldType; +import net.runelite.api.annotations.Varbit; +import net.runelite.api.annotations.Varp; + +import java.util.Collections; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Slf4j +public class LeaguesNotifier extends BaseNotifier { + private static final String AREA_UNLOCK_PREFIX = "Congratulations, you've unlocked a new area: "; + private static final String RELIC_UNLOCK_PREFIX = "Congratulations, you've unlocked a new Relic: "; + private static final Pattern TASK_REGEX = Pattern.compile("Congratulations, you've completed an? (?\\w+) task: (?.+)\\."); + + /** + * @see CS2 Reference + */ + private static final @Varbit int TASKS_COMPLETED_ID = 10046; + + /** + * @see CS2 Reference + */ + private static final @Varp int POINTS_EARNED_ID = 2614; + + /** + * Trophy name by the required points. + * + * @see Wiki Reference + * @see CS2 Reference + */ + private static final NavigableMap TROPHY_BY_POINTS; + + /** + * Mapping of each relic name to the tier (1-8). + * + * @see Wiki Reference + */ + private static final Map TIER_BY_RELIC; + + /** + * Mapping of the number of tasks required to unlock an area to the area index (1-3). + * + * @see Wiki reference + */ + private static final NavigableMap AREA_BY_TASKS; + + @Override + public boolean isEnabled() { + return config.notifyLeagues() && + client.getWorldType().contains(WorldType.SEASONAL) && + settingsManager.isNamePermitted(client.getLocalPlayer().getName()); + } + + @Override + protected String getWebhookUrl() { + return config.leaguesWebhook(); + } + + public void onGameMessage(String message) { + if (!isEnabled()) { + return; + } + if (message.startsWith(AREA_UNLOCK_PREFIX)) { + if (config.leaguesAreaUnlock()) { + String area = message.substring(AREA_UNLOCK_PREFIX.length(), message.length() - 1); + notifyAreaUnlock(area); + } + } else if (message.startsWith(RELIC_UNLOCK_PREFIX)) { + if (config.leaguesRelicUnlock()) { + String relic = message.substring(RELIC_UNLOCK_PREFIX.length(), message.length() - 1); + notifyRelicUnlock(relic); + } + } else if (config.leaguesTaskCompletion()) { + LeagueTaskDifficulty tier; + Matcher matcher = TASK_REGEX.matcher(message); + if (matcher.find() && + (tier = LeagueTaskDifficulty.TIER_BY_LOWER_NAME.get(matcher.group("tier"))) != null && + tier.ordinal() >= config.leaguesTaskMinTier().ordinal()) { + notifyTaskCompletion(tier, matcher.group("task")); + } + } + } + + private void notifyAreaUnlock(String area) { + Map.Entry unlocked = numAreasUnlocked(); + + int tasksCompleted = client.getVarbitValue(TASKS_COMPLETED_ID); + Integer tasksForNextArea = AREA_BY_TASKS.ceilingKey(tasksCompleted + 1); + Integer tasksUntilNextArea = tasksForNextArea != null ? tasksForNextArea - tasksCompleted : null; + + if (unlocked == null) { + Map.Entry entry = AREA_BY_TASKS.floorEntry(tasksCompleted); + int i = entry.getValue() != null ? entry.getValue() : 0; + unlocked = Map.entry(i, ith(i)); + } + + String playerName = Utils.getPlayerName(client); + Template text = Template.builder() + .template("%USERNAME% selected their %I_TH% region: %AREA%") + .replacementBoundary("%") + .replacement("%USERNAME%", Replacements.ofText(playerName)) + .replacement("%I_TH%", Replacements.ofText(unlocked.getValue())) + .replacement("%AREA%", Replacements.ofWiki(area, "Trailblazer Reloaded League/Areas/" + area)) + .build(); + createMessage(config.leaguesSendImage(), NotificationBody.builder() + .type(NotificationType.LEAGUES_AREA) + .text(text) + .extra(new LeaguesAreaNotificationData(area, unlocked.getKey(), tasksCompleted, tasksUntilNextArea)) + .playerName(playerName) + .seasonalWorld(true) + .build()); + } + + private void notifyRelicUnlock(String relic) { + LeagueRelicTier relicTier = TIER_BY_RELIC.get(relic); + Integer tier = relicTier != null ? relicTier.ordinal() + 1 : null; + + int points = client.getVarpValue(POINTS_EARNED_ID); + Integer pointsOfNextTier = LeagueRelicTier.TIER_BY_POINTS.ceilingKey(points + 1); + Integer pointsUntilNextTier = pointsOfNextTier != null ? pointsOfNextTier - points : null; + + String playerName = Utils.getPlayerName(client); + Template text = Template.builder() + .template("%USERNAME% unlocked a Tier %TIER% Relic: %RELIC%") + .replacementBoundary("%") + .replacement("%USERNAME%", Replacements.ofText(playerName)) + .replacement("%TIER%", Replacements.ofText(tier != null ? tier.toString() : "?")) + .replacement("%RELIC%", Replacements.ofWiki(relic)) + .build(); + createMessage(config.leaguesSendImage(), NotificationBody.builder() + .type(NotificationType.LEAGUES_RELIC) + .text(text) + .extra(new LeaguesRelicNotificationData(relic, tier, points, pointsUntilNextTier)) + .playerName(playerName) + .seasonalWorld(true) + .build()); + } + + private void notifyTaskCompletion(LeagueTaskDifficulty tier, String task) { + int taskPoints = tier.getPoints(); + int totalPoints = client.getVarpValue(POINTS_EARNED_ID); + int tasksCompleted = client.getVarbitValue(TASKS_COMPLETED_ID); + String playerName = Utils.getPlayerName(client); + + Map.Entry trophy = TROPHY_BY_POINTS.floorEntry(totalPoints); + Integer prevTrophyPoints; + if (trophy != null) { + prevTrophyPoints = TROPHY_BY_POINTS.floorKey(totalPoints - taskPoints); + } else { + prevTrophyPoints = null; + } + boolean newTrophy = trophy != null && (prevTrophyPoints == null || trophy.getKey() > prevTrophyPoints); + String justEarnedTrophy = newTrophy ? trophy.getValue() : null; + Integer nextTrophyPoints = TROPHY_BY_POINTS.ceilingKey(totalPoints + 1); + Integer pointsUntilNextTrophy = nextTrophyPoints != null ? nextTrophyPoints - totalPoints : null; + + Integer nextRelicPoints = LeagueRelicTier.TIER_BY_POINTS.ceilingKey(totalPoints + 1); + Integer pointsUntilNextRelic = nextRelicPoints != null ? nextRelicPoints - totalPoints : null; + + Template text = Template.builder() + .template(newTrophy + ? "%USERNAME% completed a %TIER% task, %TASK%, unlocking the %TROPHY% trophy!" + : "%USERNAME% completed a %TIER% task: %TASK%.") + .replacementBoundary("%") + .replacement("%USERNAME%", Replacements.ofText(playerName)) + .replacement("%TIER%", Replacements.ofText(tier.getDisplayName())) + .replacement("%TASK%", Replacements.ofWiki(task)) + .replacement("%TROPHY%", newTrophy + ? Replacements.ofWiki(trophy.getValue(), String.format("Trailblazer reloaded %s trophy", trophy.getValue().toLowerCase())) + : Replacements.ofText("?")) + .build(); + createMessage(config.leaguesSendImage(), NotificationBody.builder() + .type(NotificationType.LEAGUES_TASK) + .text(text) + .extra(new LeaguesTaskNotificationData(task, tier, taskPoints, totalPoints, tasksCompleted, pointsUntilNextRelic, pointsUntilNextTrophy, justEarnedTrophy)) + .playerName(playerName) + .seasonalWorld(true) + .build()); + } + + /** + * @return the number of areas that have been unlocked as integer and human name + * @see CS2 Reference + */ + private Map.Entry numAreasUnlocked() { + // While Jagex's code has 5 areas (2 default, 3 discretionary), + // most players think just in terms of the 3 discretionary areas, + // so we disregard Misthalin and consider Karamja as the zeroth area. + // Thus, the number of unlocked areas is bounded by 3 (instead of 5). + if (client.getVarbitValue(10666) > 0) { + return Map.entry(3, ith(3)); + } + if (client.getVarbitValue(10665) > 0) { + return Map.entry(2, ith(2)); + } + if (client.getVarbitValue(10664) > 0) { + return Map.entry(1, ith(1)); + } + if (client.getVarbitValue(10663) > 0) { + return Map.entry(0, ith(0)); // Karamja + } + return null; + } + + private static String ith(int i) { + if (i == 0) return "zeroth"; + if (i == 1) return "first"; + if (i == 2) return "second"; + if (i == 3) return "third"; + if (i == 4) return "fourth"; + if (i == 5) return "fifth"; + return String.valueOf(i); + } + + static { + AREA_BY_TASKS = Collections.unmodifiableNavigableMap( + new TreeMap<>(Map.of(0, 0, 60, 1, 200, 2, 400, 3)) + ); + + NavigableMap thresholds = new TreeMap<>(); + thresholds.put(2_500, "Bronze"); + thresholds.put(5_000, "Iron"); + thresholds.put(10_000, "Steel"); + thresholds.put(18_000, "Mithril"); + thresholds.put(28_000, "Adamant"); + thresholds.put(42_000, "Rune"); + thresholds.put(56_000, "Dragon"); + TROPHY_BY_POINTS = Collections.unmodifiableNavigableMap(thresholds); + + TIER_BY_RELIC = Map.ofEntries( + Map.entry("Endless Harvest", LeagueRelicTier.ONE), + Map.entry("Production Prodigy", LeagueRelicTier.ONE), + Map.entry("Trickster", LeagueRelicTier.ONE), + Map.entry("Fairy's Flight", LeagueRelicTier.TWO), + Map.entry("Globetrotter", LeagueRelicTier.TWO), + Map.entry("Banker's Note", LeagueRelicTier.THREE), + Map.entry("Fire Sale", LeagueRelicTier.THREE), + Map.entry("Archer's Embrace", LeagueRelicTier.FOUR), + Map.entry("Brawler's Resolve", LeagueRelicTier.FOUR), + Map.entry("Superior Sorcerer", LeagueRelicTier.FOUR), + Map.entry("Bloodthirsty", LeagueRelicTier.FIVE), + Map.entry("Infernal Gathering", LeagueRelicTier.FIVE), + Map.entry("Treasure Seeker", LeagueRelicTier.FIVE), + Map.entry("Equilibrium", LeagueRelicTier.SIX), + Map.entry("Farmer's Fortune", LeagueRelicTier.SIX), + Map.entry("Ruinous Powers", LeagueRelicTier.SIX), + Map.entry("Berserker", LeagueRelicTier.SEVEN), + Map.entry("Soul Stealer", LeagueRelicTier.SEVEN), + Map.entry("Weapon Master", LeagueRelicTier.SEVEN), + Map.entry("Guardian", LeagueRelicTier.EIGHT), + Map.entry("Executioner", LeagueRelicTier.EIGHT), + Map.entry("Undying Retribution", LeagueRelicTier.EIGHT) + ); + } +} diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java new file mode 100644 index 00000000..8b6ada1f --- /dev/null +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java @@ -0,0 +1,31 @@ +package dinkplugin.notifiers.data; + +import dinkplugin.message.Field; +import lombok.EqualsAndHashCode; +import lombok.Value; + +import java.util.ArrayList; +import java.util.List; + +@Value +@EqualsAndHashCode(callSuper = false) +public class LeaguesAreaNotificationData extends NotificationData { + String area; + Integer index; + int tasksCompleted; + Integer tasksUntilNextArea; + + @Override + public List getFields() { + List fields = new ArrayList<>(2); + fields.add( + new Field("Tasks Completed", Field.formatBlock("", String.valueOf(tasksCompleted))) + ); + if (tasksUntilNextArea != null) { + fields.add( + new Field("Tasks until next area", Field.formatBlock("", String.valueOf(tasksUntilNextArea))) + ); + } + return fields; + } +} diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java new file mode 100644 index 00000000..2a272e55 --- /dev/null +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java @@ -0,0 +1,37 @@ +package dinkplugin.notifiers.data; + +import dinkplugin.message.Field; +import lombok.EqualsAndHashCode; +import lombok.Value; + +import java.util.ArrayList; +import java.util.List; + +@Value +@EqualsAndHashCode(callSuper = false) +public class LeaguesRelicNotificationData extends NotificationData { + String relic; + Integer tier; + int totalPointsEarned; + Integer pointsUntilNextTier; + + @Override + public List getFields() { + List fields = new ArrayList<>(2); + fields.add( + new Field( + "Points Earned", + Field.formatBlock("", String.valueOf(totalPointsEarned)) + ) + ); + if (pointsUntilNextTier != null) { + fields.add( + new Field( + "Points until next tier", + Field.formatBlock("", String.valueOf(pointsUntilNextTier)) + ) + ); + } + return fields; + } +} diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java new file mode 100644 index 00000000..a0823d7b --- /dev/null +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java @@ -0,0 +1,48 @@ +package dinkplugin.notifiers.data; + +import dinkplugin.domain.LeagueTaskDifficulty; +import dinkplugin.message.Field; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +@Value +@EqualsAndHashCode(callSuper = false) +public class LeaguesTaskNotificationData extends NotificationData { + + @NotNull + String taskName; + + @NotNull + LeagueTaskDifficulty difficulty; + + int taskPoints; + int totalPoints; + int tasksCompleted; + + @Nullable // when player has already unlocked a tier 8 relic (highest) + Integer pointsUntilNextRelic; + + @Nullable // when player has already earned the dragon trophy (highest) + Integer pointsUntilNextTrophy; + + @Nullable // when player hasn't even earned the bronze trophy (lowest) + String earnedTrophy; + + @Override + public List getFields() { + List fields = new ArrayList<>(3); + fields.add(new Field("Total Tasks", Field.formatBlock("", String.valueOf(tasksCompleted)))); + fields.add(new Field("Total Points", Field.formatBlock("", String.valueOf(totalPoints)))); + if (earnedTrophy == null && pointsUntilNextRelic != null) { + fields.add(new Field("Points until next Relic", Field.formatBlock("", String.valueOf(pointsUntilNextRelic)))); + } else if (pointsUntilNextTrophy != null) { + fields.add(new Field("Points until next Trophy", Field.formatBlock("", String.valueOf(pointsUntilNextTrophy)))); + } + return fields; + } +} diff --git a/src/main/java/dinkplugin/util/Utils.java b/src/main/java/dinkplugin/util/Utils.java index 1a092c2c..1d4ee0c6 100644 --- a/src/main/java/dinkplugin/util/Utils.java +++ b/src/main/java/dinkplugin/util/Utils.java @@ -170,7 +170,10 @@ public AccountType getAccountType(@NotNull Client client) { } @Nullable - public String getChatBadge(@NotNull AccountType type) { + public String getChatBadge(@NotNull AccountType type, boolean seasonal) { + if (seasonal) { + return WIKI_IMG_BASE_URL + "Leagues_chat_badge.png"; + } switch (type) { case IRONMAN: return WIKI_IMG_BASE_URL + "Ironman_chat_badge.png"; @@ -183,7 +186,7 @@ public String getChatBadge(@NotNull AccountType type) { case HARDCORE_GROUP_IRONMAN: return WIKI_IMG_BASE_URL + "Hardcore_group_ironman_chat_badge.png"; case UNRANKED_GROUP_IRONMAN: - return WIKI_IMG_BASE_URL + "Unranked_group_ironman_chat_badge"; + return WIKI_IMG_BASE_URL + "Unranked_group_ironman_chat_badge.png"; default: return null; } From 3bbc7d2fcfa08ad17b4fa1c2cf8b0231c542b628 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 21:41:58 -0800 Subject: [PATCH 02/21] chore(relic): update rich embed field name --- .../dinkplugin/notifiers/data/LeaguesRelicNotificationData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java index 2a272e55..70f83750 100644 --- a/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java @@ -27,7 +27,7 @@ public List getFields() { if (pointsUntilNextTier != null) { fields.add( new Field( - "Points until next tier", + "Points until next Relic", Field.formatBlock("", String.valueOf(pointsUntilNextTier)) ) ); From fe7066fe4ec39afc27eb7d7907c3947f5865f9dd Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 21:53:25 -0800 Subject: [PATCH 03/21] feat: add tasksUntilNextArea to LeaguesTaskNotificationData --- src/main/java/dinkplugin/notifiers/LeaguesNotifier.java | 5 ++++- .../notifiers/data/LeaguesTaskNotificationData.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index e4397618..2ec6acae 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -158,6 +158,9 @@ private void notifyTaskCompletion(LeagueTaskDifficulty tier, String task) { int tasksCompleted = client.getVarbitValue(TASKS_COMPLETED_ID); String playerName = Utils.getPlayerName(client); + Integer nextAreaTasks = AREA_BY_TASKS.ceilingKey(tasksCompleted + 1); + Integer tasksUntilNextArea = nextAreaTasks != null ? nextAreaTasks - tasksCompleted : null; + Map.Entry trophy = TROPHY_BY_POINTS.floorEntry(totalPoints); Integer prevTrophyPoints; if (trophy != null) { @@ -188,7 +191,7 @@ private void notifyTaskCompletion(LeagueTaskDifficulty tier, String task) { createMessage(config.leaguesSendImage(), NotificationBody.builder() .type(NotificationType.LEAGUES_TASK) .text(text) - .extra(new LeaguesTaskNotificationData(task, tier, taskPoints, totalPoints, tasksCompleted, pointsUntilNextRelic, pointsUntilNextTrophy, justEarnedTrophy)) + .extra(new LeaguesTaskNotificationData(task, tier, taskPoints, totalPoints, tasksCompleted, tasksUntilNextArea, pointsUntilNextRelic, pointsUntilNextTrophy, justEarnedTrophy)) .playerName(playerName) .seasonalWorld(true) .build()); diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java index a0823d7b..b9d75f38 100644 --- a/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java @@ -24,6 +24,9 @@ public class LeaguesTaskNotificationData extends NotificationData { int totalPoints; int tasksCompleted; + @Nullable // when player has already unlocked all three regions + Integer tasksUntilNextArea; + @Nullable // when player has already unlocked a tier 8 relic (highest) Integer pointsUntilNextRelic; From fd719a9d3e0c1c68494ec1a4a5ad26822fca7b65 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 23:06:43 -0800 Subject: [PATCH 04/21] chore: add mock tests --- .../dinkplugin/notifiers/LeaguesNotifier.java | 11 +- .../notifiers/LeaguesNotifierTest.java | 324 ++++++++++++++++++ 2 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index 2ec6acae..280cd9fd 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -14,6 +14,7 @@ import net.runelite.api.WorldType; import net.runelite.api.annotations.Varbit; import net.runelite.api.annotations.Varp; +import org.jetbrains.annotations.VisibleForTesting; import java.util.Collections; import java.util.Map; @@ -31,12 +32,14 @@ public class LeaguesNotifier extends BaseNotifier { /** * @see CS2 Reference */ - private static final @Varbit int TASKS_COMPLETED_ID = 10046; + @VisibleForTesting + static final @Varbit int TASKS_COMPLETED_ID = 10046; /** * @see CS2 Reference */ - private static final @Varp int POINTS_EARNED_ID = 2614; + @VisibleForTesting + static final @Varp int POINTS_EARNED_ID = 2614; /** * Trophy name by the required points. @@ -112,7 +115,7 @@ private void notifyAreaUnlock(String area) { String playerName = Utils.getPlayerName(client); Template text = Template.builder() - .template("%USERNAME% selected their %I_TH% region: %AREA%") + .template("%USERNAME% selected their %I_TH% region: %AREA%.") .replacementBoundary("%") .replacement("%USERNAME%", Replacements.ofText(playerName)) .replacement("%I_TH%", Replacements.ofText(unlocked.getValue())) @@ -137,7 +140,7 @@ private void notifyRelicUnlock(String relic) { String playerName = Utils.getPlayerName(client); Template text = Template.builder() - .template("%USERNAME% unlocked a Tier %TIER% Relic: %RELIC%") + .template("%USERNAME% unlocked a Tier %TIER% Relic: %RELIC%.") .replacementBoundary("%") .replacement("%USERNAME%", Replacements.ofText(playerName)) .replacement("%TIER%", Replacements.ofText(tier != null ? tier.toString() : "?")) diff --git a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java new file mode 100644 index 00000000..6e570cef --- /dev/null +++ b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java @@ -0,0 +1,324 @@ +package dinkplugin.notifiers; + +import com.google.inject.testing.fieldbinder.Bind; +import dinkplugin.domain.AccountType; +import dinkplugin.domain.LeagueRelicTier; +import dinkplugin.domain.LeagueTaskDifficulty; +import dinkplugin.message.NotificationBody; +import dinkplugin.message.NotificationType; +import dinkplugin.message.templating.Replacements; +import dinkplugin.message.templating.Template; +import dinkplugin.notifiers.data.LeaguesAreaNotificationData; +import dinkplugin.notifiers.data.LeaguesRelicNotificationData; +import dinkplugin.notifiers.data.LeaguesTaskNotificationData; +import net.runelite.api.Varbits; +import net.runelite.api.WorldType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import java.util.EnumSet; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class LeaguesNotifierTest extends MockedNotifierTest { + + @Bind + @InjectMocks + LeaguesNotifier notifier; + + @Override + @BeforeEach + protected void setUp() { + super.setUp(); + + // client mocks + when(client.getWorldType()).thenReturn(EnumSet.of(WorldType.SEASONAL)); + when(client.getVarbitValue(Varbits.ACCOUNT_TYPE)).thenReturn(AccountType.IRONMAN.ordinal()); + + // config mocks + when(config.notifyLeagues()).thenReturn(true); + when(config.leaguesAreaUnlock()).thenReturn(true); + when(config.leaguesRelicUnlock()).thenReturn(true); + when(config.leaguesTaskCompletion()).thenReturn(true); + when(config.leaguesTaskMinTier()).thenReturn(LeagueTaskDifficulty.HARD); + } + + @Test + void notifyArea() { + // update client mocks + int tasksCompleted = 200; + int totalPoints = 100 * 10 + 100 * 40; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + when(client.getVarbitValue(10663)).thenReturn(2); + when(client.getVarbitValue(10664)).thenReturn(4); + when(client.getVarbitValue(10665)).thenReturn(8); + + // fire event + notifier.onGameMessage("Congratulations, you've unlocked a new area: Kandarin."); + + // verify notification + String area = "Kandarin"; + int tasksUntilNextArea = 400 - tasksCompleted; + verify(messageHandler).createMessage( + PRIMARY_WEBHOOK_URL, + false, + NotificationBody.builder() + .type(NotificationType.LEAGUES_AREA) + .text( + Template.builder() + .template(String.format("%s selected their second region: {{area}}.", PLAYER_NAME)) + .replacement("{{area}}", Replacements.ofWiki(area, "Trailblazer Reloaded League/Areas/" + area)) + .build() + ) + .extra(new LeaguesAreaNotificationData(area, 2, tasksCompleted, tasksUntilNextArea)) + .playerName(PLAYER_NAME) + .seasonalWorld(true) + .build() + ); + } + + @Test + void notifyAreaKaramja() { + // update client mocks + int tasksCompleted = 2; + int totalPoints = 2 * 10; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + when(client.getVarbitValue(10663)).thenReturn(2); + + // fire event + notifier.onGameMessage("Congratulations, you've unlocked a new area: Karamja."); + + // verify notification + String area = "Karamja"; + int tasksUntilNextArea = 60 - tasksCompleted; + verify(messageHandler).createMessage( + PRIMARY_WEBHOOK_URL, + false, + NotificationBody.builder() + .type(NotificationType.LEAGUES_AREA) + .text( + Template.builder() + .template(String.format("%s selected their zeroth region: {{area}}.", PLAYER_NAME)) + .replacement("{{area}}", Replacements.ofWiki(area, "Trailblazer Reloaded League/Areas/" + area)) + .build() + ) + .extra(new LeaguesAreaNotificationData(area, 0, tasksCompleted, tasksUntilNextArea)) + .playerName(PLAYER_NAME) + .seasonalWorld(true) + .build() + ); + } + + @Test + void notifyRelic() { + // update client mocks + int tasksCompleted = 2; + int totalPoints = 2 * 10; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + + // fire event + notifier.onGameMessage("Congratulations, you've unlocked a new Relic: Production Prodigy."); + + // verify notification + String relic = "Production Prodigy"; + int pointsUntilNextTier = LeagueRelicTier.TWO.getPoints() - totalPoints; + verify(messageHandler).createMessage( + PRIMARY_WEBHOOK_URL, + false, + NotificationBody.builder() + .type(NotificationType.LEAGUES_RELIC) + .text( + Template.builder() + .template(String.format("%s unlocked a Tier 1 Relic: {{relic}}.", PLAYER_NAME)) + .replacement("{{relic}}", Replacements.ofWiki(relic)) + .build() + ) + .extra(new LeaguesRelicNotificationData(relic, 1, totalPoints, pointsUntilNextTier)) + .playerName(PLAYER_NAME) + .seasonalWorld(true) + .build() + ); + } + + @Test + void notifyTask() { + // update client mocks + int tasksCompleted = 101; + int totalPoints = 100 * 10 + 80; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + + // fire event + notifier.onGameMessage("Congratulations, you've completed a hard task: The Frozen Door."); + + // verify notification + String taskName = "The Frozen Door"; + LeagueTaskDifficulty difficulty = LeagueTaskDifficulty.HARD; + int tasksUntilNextArea = 200 - tasksCompleted; + int pointsUntilNextRelic = LeagueRelicTier.THREE.getPoints() - totalPoints; + int pointsUntilNextTrophy = 2_500 - totalPoints; + verify(messageHandler).createMessage( + PRIMARY_WEBHOOK_URL, + false, + NotificationBody.builder() + .type(NotificationType.LEAGUES_TASK) + .text( + Template.builder() + .template(String.format("%s completed a %s task: {{task}}.", PLAYER_NAME, "Hard")) + .replacement("{{task}}", Replacements.ofWiki(taskName)) + .build() + ) + .extra(new LeaguesTaskNotificationData(taskName, difficulty, difficulty.getPoints(), totalPoints, tasksCompleted, tasksUntilNextArea, pointsUntilNextRelic, pointsUntilNextTrophy, null)) + .playerName(PLAYER_NAME) + .seasonalWorld(true) + .build() + ); + } + + @Test + void notifyTaskTrophyBronze() { + // update client mocks + int tasksCompleted = 119; + int totalPoints = 100 * 10 + 80 * 19; // 2520 >= 2500 + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + + // fire event + notifier.onGameMessage("Congratulations, you've completed a hard task: The Frozen Door."); + + // verify notification + String taskName = "The Frozen Door"; + LeagueTaskDifficulty difficulty = LeagueTaskDifficulty.HARD; + int tasksUntilNextArea = 200 - tasksCompleted; + int pointsUntilNextRelic = LeagueRelicTier.FIVE.getPoints() - totalPoints; + int pointsUntilNextTrophy = 5_000 - totalPoints; + String trophy = "Bronze"; + verify(messageHandler).createMessage( + PRIMARY_WEBHOOK_URL, + false, + NotificationBody.builder() + .type(NotificationType.LEAGUES_TASK) + .text( + Template.builder() + .template(String.format("%s completed a %s task, {{task}}, unlocking the {{trophy}} trophy!", PLAYER_NAME, "Hard")) + .replacement("{{task}}", Replacements.ofWiki(taskName)) + .replacement("{{trophy}}", Replacements.ofWiki(trophy, "Trailblazer reloaded " + trophy.toLowerCase() + " trophy")) + .build() + ) + .extra(new LeaguesTaskNotificationData(taskName, difficulty, difficulty.getPoints(), totalPoints, tasksCompleted, tasksUntilNextArea, pointsUntilNextRelic, pointsUntilNextTrophy, trophy)) + .playerName(PLAYER_NAME) + .seasonalWorld(true) + .build() + ); + } + + @Test + void notifyTaskTrophyIron() { + // update mocks + int tasksCompleted = 200; + int totalPoints = 100 * 10 + 100 * 40; // 5000 >= 5000 + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + when(config.leaguesTaskMinTier()).thenReturn(LeagueTaskDifficulty.EASY); + + // fire event + notifier.onGameMessage("Congratulations, you've completed a medium task: Equip Amy's Saw."); + + // verify notification + String taskName = "Equip Amy's Saw"; + LeagueTaskDifficulty difficulty = LeagueTaskDifficulty.MEDIUM; + int tasksUntilNextArea = 400 - tasksCompleted; + int pointsUntilNextRelic = LeagueRelicTier.SIX.getPoints() - totalPoints; + int pointsUntilNextTrophy = 10_000 - totalPoints; + String trophy = "Iron"; + verify(messageHandler).createMessage( + PRIMARY_WEBHOOK_URL, + false, + NotificationBody.builder() + .type(NotificationType.LEAGUES_TASK) + .text( + Template.builder() + .template(String.format("%s completed a %s task, {{task}}, unlocking the {{trophy}} trophy!", PLAYER_NAME, "Medium")) + .replacement("{{task}}", Replacements.ofWiki(taskName)) + .replacement("{{trophy}}", Replacements.ofWiki(trophy, "Trailblazer reloaded " + trophy.toLowerCase() + " trophy")) + .build() + ) + .extra(new LeaguesTaskNotificationData(taskName, difficulty, difficulty.getPoints(), totalPoints, tasksCompleted, tasksUntilNextArea, pointsUntilNextRelic, pointsUntilNextTrophy, trophy)) + .playerName(PLAYER_NAME) + .seasonalWorld(true) + .build() + ); + } + + @Test + void ignoreTaskTier() { + // update config mock + when(config.leaguesTaskMinTier()).thenReturn(LeagueTaskDifficulty.ELITE); + + // update client mocks + int tasksCompleted = 101; + int totalPoints = 100 * 10 + 40; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + + // fire event + notifier.onGameMessage("Congratulations, you've completed a hard task: The Frozen Door."); + + // ensure no notification occurred + verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); + } + + @Test + void testIgnored() { + // update config mocks + when(config.leaguesAreaUnlock()).thenReturn(false); + when(config.leaguesRelicUnlock()).thenReturn(false); + when(config.leaguesTaskCompletion()).thenReturn(false); + + // update client mocks + int tasksCompleted = 101; + int totalPoints = 100 * 10 + 80; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + when(client.getVarbitValue(10663)).thenReturn(2); + when(client.getVarbitValue(10664)).thenReturn(4); + + // fire event + notifier.onGameMessage("Congratulations, you've completed a hard task: The Frozen Door."); + notifier.onGameMessage("Congratulations, you've unlocked a new Relic: Production Prodigy."); + notifier.onGameMessage("Congratulations, you've unlocked a new area: Kandarin."); + + // ensure no notification occurred + verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); + } + + @Test + void testDisabled() { + // update config mocks + when(config.notifyLeagues()).thenReturn(false); + + // update client mocks + int tasksCompleted = 101; + int totalPoints = 100 * 10 + 80; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + when(client.getVarbitValue(10663)).thenReturn(2); + when(client.getVarbitValue(10664)).thenReturn(4); + + // fire event + notifier.onGameMessage("Congratulations, you've completed a hard task: The Frozen Door."); + notifier.onGameMessage("Congratulations, you've unlocked a new Relic: Production Prodigy."); + notifier.onGameMessage("Congratulations, you've unlocked a new area: Kandarin."); + + // ensure no notification occurred + verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); + } +} From 5877b178936751c79e68f54537d48eba8ad40f85 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 23:11:35 -0800 Subject: [PATCH 05/21] chore: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f340df3..ee66a13f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased +- Major: Add leagues notifier for areas, relics, and tasks. (#366) + ## 1.6.5 - Minor: Allow notifications on seasonal worlds to be ignored via advanced config. (#357) From 4fa900d78ae92e83234a59dac950c71e9d7fcf6f Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 23:17:51 -0800 Subject: [PATCH 06/21] chore(region): update rich embed field name --- .../dinkplugin/notifiers/data/LeaguesAreaNotificationData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java index 8b6ada1f..04a1fce1 100644 --- a/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java @@ -23,7 +23,7 @@ public List getFields() { ); if (tasksUntilNextArea != null) { fields.add( - new Field("Tasks until next area", Field.formatBlock("", String.valueOf(tasksUntilNextArea))) + new Field("Tasks until next Area", Field.formatBlock("", String.valueOf(tasksUntilNextArea))) ); } return fields; From af1729fb53102a85c57d69d2081ed21d71fe73e8 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 23:52:55 -0800 Subject: [PATCH 07/21] chore: update readme --- README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/README.md b/README.md index 4eb88045..d9b18df4 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ To use this plugin, a webhook URL is required; you can obtain one from Discord w - [Player Kills](#player-kills): Sends a webhook message upon killing another player (while hitsplats are still visible) - [Group Storage](#group-storage): Sends a webhook message upon Group Ironman Shared Bank transactions (i.e., depositing or withdrawing items) - [Grand Exchange](#grand-exchange): Sends a webhook message upon buying or selling items on the GE (with customizable value threshold) +- [Leagues](#leagues): Sends a webhook message upon completing a Leagues IV task or unlocking a region/relic ## Other Setup @@ -759,6 +760,81 @@ See [javadocs](https://static.runelite.net/api/runelite-api/net/runelite/api/Gra +### Leagues: + +Leagues notifications include: region unlocked, relic unlocked, and task completed (with customizable difficulty threshold). + +Each of these events can be independently enabled or disabled in the notifier settings. + +
+ JSON for Area Unlock Notifications: + +```json5 +{ + "type": "LEAGUES_AREA", + "content": "%USERNAME% selected their second region: Kandarin.", + "playerName": "%USERNAME%", + "accountType": "IRONMAN", + "seasonalWorld": true, + "extra": { + "area": "Kandarin", + "index": 2, + "tasksCompleted": 200, + "tasksUntilNextArea": 200 + } +} +``` + +
+ +
+ JSON for Relic Chosen Notifications: + +```json5 +{ + "type": "LEAGUES_RELIC", + "content": "%USERNAME% unlocked a Tier 1 Relic: Production Prodigy.", + "playerName": "%USERNAME%", + "accountType": "IRONMAN", + "seasonalWorld": true, + "extra": { + "relic": "Production Prodigy", + "tier": 1, + "totalPointsEarned": 20, + "pointsUntilNextTier": 480 + } +} +``` + +
+ +
+ JSON for Task Completed Notifications: + +```json5 +{ + "type": "LEAGUES_TASK", + "content": "%USERNAME% completed a Easy task: Pickpocket a Citizen.", + "playerName": "%USERNAME%", + "accountType": "IRONMAN", + "seasonalWorld": true, + "extra": { + "taskName": "Pickpocket a Citizen", + "difficulty": "EASY", + "taskPoints": 10, + "totalPoints": 30, + "tasksCompleted": 3, + "pointsUntilNextRelic": 470, + "pointsUntilNextTrophy": 2470 + } +} +``` + +
+ +Note: Fields like `tasksUntilNextArea`, `pointsUntilNextRelic`, and `pointsUntilNextTrophy` can be omitted +if there is no next level of progression (i.e., all three regions selected, all relic tiers unlocked, all trophies acquired). + ### Metadata: On login, Dink can submit a character summary containing data that spans multiple notifiers to a custom webhook handler (configurable in the `Advanced` section). This login notification is delayed by at least 5 seconds in order to gather all of the relevant data. However, `collectionLog` data can be missing if the user does not have the Character Summary tab selected (since the client otherwise is not sent that data). From f69d250d0259598a84294bf88dcbdfc20940e2e2 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Wed, 15 Nov 2023 23:58:46 -0800 Subject: [PATCH 08/21] chore(readme): clarify area index --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d9b18df4..d7ee5ff0 100644 --- a/README.md +++ b/README.md @@ -785,6 +785,10 @@ Each of these events can be independently enabled or disabled in the notifier se } ``` +Note: `index` refers to the order of region unlocks. +Here, Kandarin was the second region selected. +For all players, Karamja is the *zeroth* region selected (and there is no notification for Misthalin). +
From 920f6379a490c5e950e7454255a8540646c808c5 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 00:00:49 -0800 Subject: [PATCH 09/21] chore(readme): markdown formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7ee5ff0..39ad2706 100644 --- a/README.md +++ b/README.md @@ -787,7 +787,7 @@ Each of these events can be independently enabled or disabled in the notifier se Note: `index` refers to the order of region unlocks. Here, Kandarin was the second region selected. -For all players, Karamja is the *zeroth* region selected (and there is no notification for Misthalin). +For all players, Karamja is the _zeroth_ region selected (and there is no notification for Misthalin).
From 9df760d02689b313daaeb588436145387d62ff07 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 00:06:36 -0800 Subject: [PATCH 10/21] feat: add requiredPoints to relic metadata --- README.md | 3 ++- .../dinkplugin/notifiers/LeaguesNotifier.java | 3 ++- .../data/LeaguesRelicNotificationData.java | 18 +++++++++++++++--- .../notifiers/LeaguesNotifierTest.java | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 39ad2706..1ff241d1 100644 --- a/README.md +++ b/README.md @@ -804,7 +804,8 @@ For all players, Karamja is the _zeroth_ region selected (and there is no notifi "extra": { "relic": "Production Prodigy", "tier": 1, - "totalPointsEarned": 20, + "requiredPoints": 0, + "totalPoints": 20, "pointsUntilNextTier": 480 } } diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index 280cd9fd..f43e7570 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -133,6 +133,7 @@ private void notifyAreaUnlock(String area) { private void notifyRelicUnlock(String relic) { LeagueRelicTier relicTier = TIER_BY_RELIC.get(relic); Integer tier = relicTier != null ? relicTier.ordinal() + 1 : null; + Integer requiredPoints = relicTier != null ? relicTier.getPoints() : null; int points = client.getVarpValue(POINTS_EARNED_ID); Integer pointsOfNextTier = LeagueRelicTier.TIER_BY_POINTS.ceilingKey(points + 1); @@ -149,7 +150,7 @@ private void notifyRelicUnlock(String relic) { createMessage(config.leaguesSendImage(), NotificationBody.builder() .type(NotificationType.LEAGUES_RELIC) .text(text) - .extra(new LeaguesRelicNotificationData(relic, tier, points, pointsUntilNextTier)) + .extra(new LeaguesRelicNotificationData(relic, tier, requiredPoints, points, pointsUntilNextTier)) .playerName(playerName) .seasonalWorld(true) .build()); diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java index 70f83750..8c14a91e 100644 --- a/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java @@ -3,6 +3,8 @@ import dinkplugin.message.Field; import lombok.EqualsAndHashCode; import lombok.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -10,9 +12,19 @@ @Value @EqualsAndHashCode(callSuper = false) public class LeaguesRelicNotificationData extends NotificationData { + + @NotNull String relic; + + @Nullable // if dink did not recognize the relic name Integer tier; - int totalPointsEarned; + + @Nullable // if dink did not recognize the relic name + Integer requiredPoints; + + int totalPoints; + + @Nullable // if relics for all 8 tiers have now been unlocked Integer pointsUntilNextTier; @Override @@ -20,8 +32,8 @@ public List getFields() { List fields = new ArrayList<>(2); fields.add( new Field( - "Points Earned", - Field.formatBlock("", String.valueOf(totalPointsEarned)) + "Total Points", + Field.formatBlock("", String.valueOf(totalPoints)) ) ); if (pointsUntilNextTier != null) { diff --git a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java index 6e570cef..e9b96822 100644 --- a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java +++ b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java @@ -141,7 +141,7 @@ void notifyRelic() { .replacement("{{relic}}", Replacements.ofWiki(relic)) .build() ) - .extra(new LeaguesRelicNotificationData(relic, 1, totalPoints, pointsUntilNextTier)) + .extra(new LeaguesRelicNotificationData(relic, 1, 0, totalPoints, pointsUntilNextTier)) .playerName(PLAYER_NAME) .seasonalWorld(true) .build() From a86e6f767d4ac115cdbd7409fafd9b15964cc8b4 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 00:11:04 -0800 Subject: [PATCH 11/21] chore(readme): add trophy unlock json example --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 1ff241d1..28575bb0 100644 --- a/README.md +++ b/README.md @@ -837,6 +837,32 @@ For all players, Karamja is the _zeroth_ region selected (and there is no notifi +
+ JSON for Task Notifications that unlocked a Trophy: + +```json5 +{ + "type": "LEAGUES_TASK", + "content": "%USERNAME% completed a Hard task, The Frozen Door, unlocking the Bronze trophy!", + "playerName": "%USERNAME%", + "accountType": "IRONMAN", + "seasonalWorld": true, + "extra": { + "taskName": "The Frozen Door", + "difficulty": "HARD", + "taskPoints": 80, + "totalPoints": 2520, + "tasksCompleted": 119, + "tasksUntilNextArea": 81, + "pointsUntilNextRelic": 1480, + "pointsUntilNextTrophy": 2480, + "earnedTrophy": "Bronze" + } +} +``` + +
+ Note: Fields like `tasksUntilNextArea`, `pointsUntilNextRelic`, and `pointsUntilNextTrophy` can be omitted if there is no next level of progression (i.e., all three regions selected, all relic tiers unlocked, all trophies acquired). From ff295afa124ec8c17274f8614065e49451957a0e Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 00:18:11 -0800 Subject: [PATCH 12/21] chore: link task names to general league tasks wiki article --- src/main/java/dinkplugin/notifiers/LeaguesNotifier.java | 2 +- src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index f43e7570..8b61ff75 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -187,7 +187,7 @@ private void notifyTaskCompletion(LeagueTaskDifficulty tier, String task) { .replacementBoundary("%") .replacement("%USERNAME%", Replacements.ofText(playerName)) .replacement("%TIER%", Replacements.ofText(tier.getDisplayName())) - .replacement("%TASK%", Replacements.ofWiki(task)) + .replacement("%TASK%", Replacements.ofWiki(task, "Trailblazer_Reloaded_League/Tasks")) .replacement("%TROPHY%", newTrophy ? Replacements.ofWiki(trophy.getValue(), String.format("Trailblazer reloaded %s trophy", trophy.getValue().toLowerCase())) : Replacements.ofText("?")) diff --git a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java index e9b96822..96dd2ee6 100644 --- a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java +++ b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java @@ -173,7 +173,7 @@ void notifyTask() { .text( Template.builder() .template(String.format("%s completed a %s task: {{task}}.", PLAYER_NAME, "Hard")) - .replacement("{{task}}", Replacements.ofWiki(taskName)) + .replacement("{{task}}", Replacements.ofWiki(taskName, "Trailblazer_Reloaded_League/Tasks")) .build() ) .extra(new LeaguesTaskNotificationData(taskName, difficulty, difficulty.getPoints(), totalPoints, tasksCompleted, tasksUntilNextArea, pointsUntilNextRelic, pointsUntilNextTrophy, null)) @@ -209,7 +209,7 @@ void notifyTaskTrophyBronze() { .text( Template.builder() .template(String.format("%s completed a %s task, {{task}}, unlocking the {{trophy}} trophy!", PLAYER_NAME, "Hard")) - .replacement("{{task}}", Replacements.ofWiki(taskName)) + .replacement("{{task}}", Replacements.ofWiki(taskName, "Trailblazer_Reloaded_League/Tasks")) .replacement("{{trophy}}", Replacements.ofWiki(trophy, "Trailblazer reloaded " + trophy.toLowerCase() + " trophy")) .build() ) @@ -247,7 +247,7 @@ void notifyTaskTrophyIron() { .text( Template.builder() .template(String.format("%s completed a %s task, {{task}}, unlocking the {{trophy}} trophy!", PLAYER_NAME, "Medium")) - .replacement("{{task}}", Replacements.ofWiki(taskName)) + .replacement("{{task}}", Replacements.ofWiki(taskName, "Trailblazer_Reloaded_League/Tasks")) .replacement("{{trophy}}", Replacements.ofWiki(trophy, "Trailblazer reloaded " + trophy.toLowerCase() + " trophy")) .build() ) From 6089fd53ff85660a37831586434299a2aa5065d6 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 00:26:14 -0800 Subject: [PATCH 13/21] refactor: move some varbit ids to constant fields --- .../dinkplugin/notifiers/LeaguesNotifier.java | 15 ++++++++++----- .../notifiers/LeaguesNotifierTest.java | 16 ++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index 8b61ff75..abbc626d 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -41,6 +41,12 @@ public class LeaguesNotifier extends BaseNotifier { @VisibleForTesting static final @Varp int POINTS_EARNED_ID = 2614; + /** + * @see CS2 Reference + */ + @VisibleForTesting + static final @Varbit int FIVE_AREAS = 10666, FOUR_AREAS = 10665, THREE_AREAS = 10664, TWO_AREAS = 10663; + /** * Trophy name by the required points. * @@ -203,23 +209,22 @@ private void notifyTaskCompletion(LeagueTaskDifficulty tier, String task) { /** * @return the number of areas that have been unlocked as integer and human name - * @see CS2 Reference */ private Map.Entry numAreasUnlocked() { // While Jagex's code has 5 areas (2 default, 3 discretionary), // most players think just in terms of the 3 discretionary areas, // so we disregard Misthalin and consider Karamja as the zeroth area. // Thus, the number of unlocked areas is bounded by 3 (instead of 5). - if (client.getVarbitValue(10666) > 0) { + if (client.getVarbitValue(FIVE_AREAS) > 0) { return Map.entry(3, ith(3)); } - if (client.getVarbitValue(10665) > 0) { + if (client.getVarbitValue(FOUR_AREAS) > 0) { return Map.entry(2, ith(2)); } - if (client.getVarbitValue(10664) > 0) { + if (client.getVarbitValue(THREE_AREAS) > 0) { return Map.entry(1, ith(1)); } - if (client.getVarbitValue(10663) > 0) { + if (client.getVarbitValue(TWO_AREAS) > 0) { return Map.entry(0, ith(0)); // Karamja } return null; diff --git a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java index 96dd2ee6..8be56ed9 100644 --- a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java +++ b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java @@ -55,9 +55,9 @@ void notifyArea() { int totalPoints = 100 * 10 + 100 * 40; when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); - when(client.getVarbitValue(10663)).thenReturn(2); - when(client.getVarbitValue(10664)).thenReturn(4); - when(client.getVarbitValue(10665)).thenReturn(8); + when(client.getVarbitValue(LeaguesNotifier.TWO_AREAS)).thenReturn(2); + when(client.getVarbitValue(LeaguesNotifier.THREE_AREAS)).thenReturn(4); + when(client.getVarbitValue(LeaguesNotifier.FOUR_AREAS)).thenReturn(8); // fire event notifier.onGameMessage("Congratulations, you've unlocked a new area: Kandarin."); @@ -90,7 +90,7 @@ void notifyAreaKaramja() { int totalPoints = 2 * 10; when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); - when(client.getVarbitValue(10663)).thenReturn(2); + when(client.getVarbitValue(LeaguesNotifier.TWO_AREAS)).thenReturn(2); // fire event notifier.onGameMessage("Congratulations, you've unlocked a new area: Karamja."); @@ -288,8 +288,8 @@ void testIgnored() { int totalPoints = 100 * 10 + 80; when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); - when(client.getVarbitValue(10663)).thenReturn(2); - when(client.getVarbitValue(10664)).thenReturn(4); + when(client.getVarbitValue(LeaguesNotifier.TWO_AREAS)).thenReturn(2); + when(client.getVarbitValue(LeaguesNotifier.THREE_AREAS)).thenReturn(4); // fire event notifier.onGameMessage("Congratulations, you've completed a hard task: The Frozen Door."); @@ -310,8 +310,8 @@ void testDisabled() { int totalPoints = 100 * 10 + 80; when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); - when(client.getVarbitValue(10663)).thenReturn(2); - when(client.getVarbitValue(10664)).thenReturn(4); + when(client.getVarbitValue(LeaguesNotifier.TWO_AREAS)).thenReturn(2); + when(client.getVarbitValue(LeaguesNotifier.THREE_AREAS)).thenReturn(4); // fire event notifier.onGameMessage("Congratulations, you've completed a hard task: The Frozen Door."); From e91baba6782be5a61d7e4e20c646a10b73185d98 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 00:33:13 -0800 Subject: [PATCH 14/21] chore: clarify leagues webhook override --- src/main/java/dinkplugin/DinkPluginConfig.java | 3 ++- src/main/java/dinkplugin/notifiers/LeaguesNotifier.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java index e1763a3f..c95ba9e8 100644 --- a/src/main/java/dinkplugin/DinkPluginConfig.java +++ b/src/main/java/dinkplugin/DinkPluginConfig.java @@ -559,7 +559,8 @@ default String grandExchangeWebhook() { @ConfigItem( keyName = "leaguesWebhook", name = "Leagues Webhook Override", - description = "If non-empty, Leagues messages are sent to this URL, instead of the primary URL", + description = "If non-empty, Leagues messages are sent to this URL, instead of the primary URL.
" + + "Note: this only applies to the Leagues notifier, not every notifier in a seasonal world", position = -1, section = webhookSection ) diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index abbc626d..dc045f9e 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -48,7 +48,7 @@ public class LeaguesNotifier extends BaseNotifier { static final @Varbit int FIVE_AREAS = 10666, FOUR_AREAS = 10665, THREE_AREAS = 10664, TWO_AREAS = 10663; /** - * Trophy name by the required points. + * Trophy name by the required points, in a binary search tree. * * @see Wiki Reference * @see CS2 Reference @@ -63,7 +63,7 @@ public class LeaguesNotifier extends BaseNotifier { private static final Map TIER_BY_RELIC; /** - * Mapping of the number of tasks required to unlock an area to the area index (1-3). + * Mapping of the number of tasks required to unlock an area to the area index (0-3). * * @see Wiki reference */ From 045b248b4d72e002f95494895aa967e66d16373e Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 01:29:47 -0800 Subject: [PATCH 15/21] chore: loosen default leaguesTaskMinTier --- src/main/java/dinkplugin/DinkPluginConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java index c95ba9e8..d4160150 100644 --- a/src/main/java/dinkplugin/DinkPluginConfig.java +++ b/src/main/java/dinkplugin/DinkPluginConfig.java @@ -1745,7 +1745,7 @@ default boolean leaguesTaskCompletion() { section = leaguesSection ) default LeagueTaskDifficulty leaguesTaskMinTier() { - return LeagueTaskDifficulty.HARD; + return LeagueTaskDifficulty.EASY; } } From f72a14f9288bedf39849af776ee5ddbe08da1354 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 01:41:19 -0800 Subject: [PATCH 16/21] fix: handle unknown relic names more gracefully --- .../dinkplugin/notifiers/LeaguesNotifier.java | 15 +++++++++++---- .../data/LeaguesRelicNotificationData.java | 5 ----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index dc045f9e..80dd9073 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -137,14 +137,21 @@ private void notifyAreaUnlock(String area) { } private void notifyRelicUnlock(String relic) { - LeagueRelicTier relicTier = TIER_BY_RELIC.get(relic); - Integer tier = relicTier != null ? relicTier.ordinal() + 1 : null; - Integer requiredPoints = relicTier != null ? relicTier.getPoints() : null; - int points = client.getVarpValue(POINTS_EARNED_ID); Integer pointsOfNextTier = LeagueRelicTier.TIER_BY_POINTS.ceilingKey(points + 1); Integer pointsUntilNextTier = pointsOfNextTier != null ? pointsOfNextTier - points : null; + LeagueRelicTier relicTier = TIER_BY_RELIC.get(relic); + if (relicTier == null) { + // shouldn't happen, but just to be safe + log.warn("Unknown relic encountered: {}", relic); + if (points >= 0) { + relicTier = LeagueRelicTier.TIER_BY_POINTS.floorEntry(points).getValue(); + } + } + Integer tier = relicTier != null ? relicTier.ordinal() + 1 : null; + Integer requiredPoints = relicTier != null ? relicTier.getPoints() : null; + String playerName = Utils.getPlayerName(client); Template text = Template.builder() .template("%USERNAME% unlocked a Tier %TIER% Relic: %RELIC%.") diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java index 8c14a91e..a89c74e9 100644 --- a/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesRelicNotificationData.java @@ -15,13 +15,8 @@ public class LeaguesRelicNotificationData extends NotificationData { @NotNull String relic; - - @Nullable // if dink did not recognize the relic name Integer tier; - - @Nullable // if dink did not recognize the relic name Integer requiredPoints; - int totalPoints; @Nullable // if relics for all 8 tiers have now been unlocked From 7d0604f7db4b602d6b471b1682f7b723a5965cc8 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Thu, 16 Nov 2023 20:52:29 -0800 Subject: [PATCH 17/21] fix: ensure computeEmbeds can read seasonal status --- .../java/dinkplugin/message/DiscordMessageHandler.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/dinkplugin/message/DiscordMessageHandler.java b/src/main/java/dinkplugin/message/DiscordMessageHandler.java index 2a33b38c..be712525 100644 --- a/src/main/java/dinkplugin/message/DiscordMessageHandler.java +++ b/src/main/java/dinkplugin/message/DiscordMessageHandler.java @@ -219,12 +219,12 @@ private NotificationBody enrichBody(NotificationBody mBody, boolean sendIm } } - NotificationBody.NotificationBodyBuilder builder = mBody.toBuilder(); - - if (!config.ignoreSeasonal() && !mBody.isSeasonalWorld()) { - builder.seasonalWorld(client.getWorldType().contains(WorldType.SEASONAL)); + if (!config.ignoreSeasonal() && !mBody.isSeasonalWorld() && client.getWorldType().contains(WorldType.SEASONAL)) { + mBody = mBody.withSeasonalWorld(true); } + NotificationBody.NotificationBodyBuilder builder = mBody.toBuilder(); + if (config.sendDiscordUser()) { builder.discordUser(DiscordProfile.of(discordService.getCurrentUser())); } From b15703eb802b9ddd0490713b981b33bd30321e9f Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Fri, 17 Nov 2023 14:14:56 +0100 Subject: [PATCH 18/21] unrelated commit: add some tests for Utils.sanitize --- src/test/java/dinkplugin/util/UtilsTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/dinkplugin/util/UtilsTest.java b/src/test/java/dinkplugin/util/UtilsTest.java index 889ce1ca..1b36998d 100644 --- a/src/test/java/dinkplugin/util/UtilsTest.java +++ b/src/test/java/dinkplugin/util/UtilsTest.java @@ -112,4 +112,17 @@ void regexify() { assertFalse(m.matcher("iron pickaxe").find()); } + @Test + void sanitize() { + assertEquals("Congratulations, you've unlocked a new Relic: Archer's Embrace.", Utils.sanitize("Congratulations, you've unlocked a new Relic: Archer's Embrace.")); + assertEquals("Congratulations, you've completed an easy task: Obtain a Gem While Mining.", Utils.sanitize("Congratulations, you've completed an easy task: Obtain a Gem While Mining.")); + + assertEquals("", Utils.sanitize(null)); + assertEquals("", Utils.sanitize("")); + + assertEquals("foo\nbar", Utils.sanitize("foo
bar")); + + assertEquals("foo bar", Utils.sanitize("foo\u00A0bar")); + } + } From 7e2172db0709f7a2287bf8242b19ac616cf651d3 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Fri, 17 Nov 2023 10:55:47 -0800 Subject: [PATCH 19/21] docs: clarify more metadata field nullability --- .../java/dinkplugin/notifiers/LeaguesNotifier.java | 3 +-- .../notifiers/data/LeaguesAreaNotificationData.java | 10 +++++++++- .../notifiers/data/LeaguesTaskNotificationData.java | 8 ++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index 80dd9073..94c44c0d 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -114,8 +114,7 @@ private void notifyAreaUnlock(String area) { Integer tasksUntilNextArea = tasksForNextArea != null ? tasksForNextArea - tasksCompleted : null; if (unlocked == null) { - Map.Entry entry = AREA_BY_TASKS.floorEntry(tasksCompleted); - int i = entry.getValue() != null ? entry.getValue() : 0; + int i = AREA_BY_TASKS.floorEntry(Math.max(tasksCompleted, 0)).getValue(); unlocked = Map.entry(i, ith(i)); } diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java index 04a1fce1..0cd487c9 100644 --- a/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesAreaNotificationData.java @@ -3,6 +3,8 @@ import dinkplugin.message.Field; import lombok.EqualsAndHashCode; import lombok.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -10,9 +12,15 @@ @Value @EqualsAndHashCode(callSuper = false) public class LeaguesAreaNotificationData extends NotificationData { + + @NotNull String area; - Integer index; + + int index; + int tasksCompleted; + + @Nullable // if player has already unlocked all three regions Integer tasksUntilNextArea; @Override diff --git a/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java b/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java index b9d75f38..f03b18bb 100644 --- a/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java +++ b/src/main/java/dinkplugin/notifiers/data/LeaguesTaskNotificationData.java @@ -24,16 +24,16 @@ public class LeaguesTaskNotificationData extends NotificationData { int totalPoints; int tasksCompleted; - @Nullable // when player has already unlocked all three regions + @Nullable // if player has already unlocked all three regions Integer tasksUntilNextArea; - @Nullable // when player has already unlocked a tier 8 relic (highest) + @Nullable // if player has already unlocked a tier 8 relic (highest) Integer pointsUntilNextRelic; - @Nullable // when player has already earned the dragon trophy (highest) + @Nullable // if player has already earned the dragon trophy (highest) Integer pointsUntilNextTrophy; - @Nullable // when player hasn't even earned the bronze trophy (lowest) + @Nullable // if the player did not earn a new trophy with this task completion String earnedTrophy; @Override From d0cad9d0fb5a69a58314d1815f602cfe6b59d979 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Fri, 17 Nov 2023 10:59:35 -0800 Subject: [PATCH 20/21] refactor: prefer nested if over assignment within conditional --- .../java/dinkplugin/notifiers/LeaguesNotifier.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java index 94c44c0d..375b2046 100644 --- a/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java +++ b/src/main/java/dinkplugin/notifiers/LeaguesNotifier.java @@ -96,12 +96,12 @@ public void onGameMessage(String message) { notifyRelicUnlock(relic); } } else if (config.leaguesTaskCompletion()) { - LeagueTaskDifficulty tier; Matcher matcher = TASK_REGEX.matcher(message); - if (matcher.find() && - (tier = LeagueTaskDifficulty.TIER_BY_LOWER_NAME.get(matcher.group("tier"))) != null && - tier.ordinal() >= config.leaguesTaskMinTier().ordinal()) { - notifyTaskCompletion(tier, matcher.group("task")); + if (matcher.find()) { + LeagueTaskDifficulty tier = LeagueTaskDifficulty.TIER_BY_LOWER_NAME.get(matcher.group("tier")); + if (tier != null && tier.ordinal() >= config.leaguesTaskMinTier().ordinal()) { + notifyTaskCompletion(tier, matcher.group("task")); + } } } } From 1ac74f99b7b7d9603ddc0c47073c888d72f098d0 Mon Sep 17 00:00:00 2001 From: iProdigy Date: Fri, 17 Nov 2023 11:01:57 -0800 Subject: [PATCH 21/21] chore: add another test case --- .../dinkplugin/notifiers/LeaguesNotifierTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java index 8be56ed9..af61408d 100644 --- a/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java +++ b/src/test/java/dinkplugin/notifiers/LeaguesNotifierTest.java @@ -321,4 +321,19 @@ void testDisabled() { // ensure no notification occurred verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); } + + @Test + void ignoreIrrelevant() { + // update client mocks + int tasksCompleted = 101; + int totalPoints = 100 * 10 + 80; + when(client.getVarbitValue(LeaguesNotifier.TASKS_COMPLETED_ID)).thenReturn(tasksCompleted); + when(client.getVarpValue(LeaguesNotifier.POINTS_EARNED_ID)).thenReturn(totalPoints); + + // fire event + notifier.onGameMessage("Congratulations, you've completed a hard combat task: Ready to Pounce."); + + // ensure no notification occurred + verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); + } }