Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add leagues notifier for areas, relics, and tasks #366

Merged
merged 21 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3643585
feat: add leagues notifier for areas, relics, and tasks
iProdigy Nov 16, 2023
3bbc7d2
chore(relic): update rich embed field name
iProdigy Nov 16, 2023
fe7066f
feat: add tasksUntilNextArea to LeaguesTaskNotificationData
iProdigy Nov 16, 2023
fd719a9
chore: add mock tests
iProdigy Nov 16, 2023
5877b17
chore: update changelog
iProdigy Nov 16, 2023
4fa900d
chore(region): update rich embed field name
iProdigy Nov 16, 2023
af1729f
chore: update readme
iProdigy Nov 16, 2023
f69d250
chore(readme): clarify area index
iProdigy Nov 16, 2023
920f637
chore(readme): markdown formatting
iProdigy Nov 16, 2023
9df760d
feat: add requiredPoints to relic metadata
iProdigy Nov 16, 2023
a86e6f7
chore(readme): add trophy unlock json example
iProdigy Nov 16, 2023
ff295af
chore: link task names to general league tasks wiki article
iProdigy Nov 16, 2023
6089fd5
refactor: move some varbit ids to constant fields
iProdigy Nov 16, 2023
e91baba
chore: clarify leagues webhook override
iProdigy Nov 16, 2023
045b248
chore: loosen default leaguesTaskMinTier
iProdigy Nov 16, 2023
f72a14f
fix: handle unknown relic names more gracefully
iProdigy Nov 16, 2023
7d0604f
fix: ensure computeEmbeds can read seasonal status
iProdigy Nov 17, 2023
b15703e
unrelated commit: add some tests for Utils.sanitize
pajlada Nov 17, 2023
7e2172d
docs: clarify more metadata field nullability
iProdigy Nov 17, 2023
d0cad9d
refactor: prefer nested if over assignment within conditional
iProdigy Nov 17, 2023
1ac74f9
chore: add another test case
iProdigy Nov 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -759,6 +760,112 @@ See [javadocs](https://static.runelite.net/api/runelite-api/net/runelite/api/Gra

</details>

### 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.

<details>
<summary>JSON for Area Unlock Notifications:</summary>

```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
}
}
```

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).

</details>

<details>
<summary>JSON for Relic Chosen Notifications:</summary>

```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,
"requiredPoints": 0,
"totalPoints": 20,
"pointsUntilNextTier": 480
}
}
```

</details>

<details>
<summary>JSON for Task Completed Notifications:</summary>

```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
}
}
```

</details>

<details>
<summary>JSON for Task Notifications that unlocked a Trophy:</summary>

```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"
}
}
```

</details>

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).
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/dinkplugin/DinkPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -186,6 +188,7 @@ public void onChatMessage(ChatMessage message) {
combatTaskNotifier.onGameMessage(chatMessage);
deathNotifier.onGameMessage(chatMessage);
speedrunNotifier.onGameMessage(chatMessage);
leaguesNotifier.onGameMessage(chatMessage);
break;

case FRIENDSCHATNOTIFICATION:
Expand Down
90 changes: 89 additions & 1 deletion src/main/java/dinkplugin/DinkPluginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.<br/>" +
"Note: the Leagues-specific notifier uses an independent config toggle",
position = 1015,
section = advancedSection
)
Expand Down Expand Up @@ -546,6 +556,18 @@ 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.<br/>" +
"Note: this only applies to the Leagues notifier, not every notifier in a seasonal world",
position = -1,
section = webhookSection
)
default String leaguesWebhook() {
return "";
}

@ConfigItem(
keyName = "collectionLogEnabled",
name = "Enable collection log",
Expand Down Expand Up @@ -1660,4 +1682,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.EASY;
}

}
38 changes: 38 additions & 0 deletions src/main/java/dinkplugin/domain/LeagueRelicTier.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://oldschool.runescape.wiki/w/Trailblazer_Reloaded_League/Relics">Wiki Reference</a>
*/
private final int points;

public static final NavigableMap<Integer, LeagueRelicTier> TIER_BY_POINTS;

static {
NavigableMap<Integer, LeagueRelicTier> tiers = new TreeMap<>();
for (LeagueRelicTier tier : values()) {
tiers.put(tier.getPoints(), tier);
}
TIER_BY_POINTS = Collections.unmodifiableNavigableMap(tiers);
}
}
37 changes: 37 additions & 0 deletions src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://oldschool.runescape.wiki/w/Trailblazer_Reloaded_League/Tasks">Wiki Reference</a>
*/
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<String, LeagueTaskDifficulty> TIER_BY_LOWER_NAME = Collections.unmodifiableMap(
Arrays.stream(values()).collect(Collectors.toMap(t -> t.name().toLowerCase(), Function.identity()))
);
}
10 changes: 5 additions & 5 deletions src/main/java/dinkplugin/message/DiscordMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,12 @@ private NotificationBody<?> enrichBody(NotificationBody<?> mBody, boolean sendIm
}
}

NotificationBody.NotificationBodyBuilder<?> builder = mBody.toBuilder();

if (!config.ignoreSeasonal()) {
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()));
}
Expand Down Expand Up @@ -333,7 +333,7 @@ private static List<Embed> 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))
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/dinkplugin/message/NotificationType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading