Skip to content

Commit

Permalink
feat: fire notification on xp milestones
Browse files Browse the repository at this point in the history
  • Loading branch information
iProdigy committed Apr 14, 2024
1 parent 998c588 commit 65f9ab2
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 16 deletions.
29 changes: 26 additions & 3 deletions src/main/java/dinkplugin/DinkPluginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ default boolean levelNotifyCombat() {
@ConfigItem(
keyName = "levelInterval",
name = "Notify Interval",
description = "Interval between when a notification should be sent",
description = "Level interval between when a notification should be sent",
position = 24,
section = levelSection
)
Expand Down Expand Up @@ -776,14 +776,37 @@ default int levelIntervalOverride() {
return 0;
}

@ConfigItem(
keyName = "xpInterval",
name = "XP Interval",
description = "XP interval at which to fire notifications (disabled if set to 0)",
position = 27,
section = levelSection
)
default int xpInterval() {
return 5_000_000;
}

@ConfigItem(
keyName = "ignoreXpBelowMax",
name = "Skip XP Interval below Lvl 99",
description = "Prevent XP interval notifications for levels below 99",
position = 28,
section = levelSection
)
default boolean ignoreXpBelowMax() {
return true;
}

@ConfigItem(
keyName = "levelNotifMessage",
name = "Notification Message",
description = "The message to be sent through the webhook.<br/>" +
"Use %USERNAME% to insert your username<br/>" +
"Use %SKILL% to insert the levelled skill(s)<br/>" +
"Use %TOTAL_LEVEL% to insert the updated total level",
position = 27,
"Use %TOTAL_LEVEL% to insert the updated total level<br/>" +
"Use %TOTAL_XP% to insert the updated overall experience",
position = 29,
section = levelSection
)
default String levelNotifyMessage() {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dinkplugin/message/NotificationType.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public enum NotificationType {
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"),
TRADE("Player Trade", "trade.png", WIKI_IMG_BASE_URL + "Inventory.png"),
CHAT("Chat Notification", "chat.png", WIKI_IMG_BASE_URL + "Toggle_Chat_effects.png");
CHAT("Chat Notification", "chat.png", WIKI_IMG_BASE_URL + "Toggle_Chat_effects.png"),
XP("XP Milestone", "xpImage.png", WIKI_IMG_BASE_URL + "Lamp.png");

private final String title;

Expand Down
102 changes: 91 additions & 11 deletions src/main/java/dinkplugin/notifiers/LevelNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import dinkplugin.message.templating.Template;
import dinkplugin.message.templating.impl.JoiningReplacement;
import dinkplugin.notifiers.data.LevelNotificationData;
import dinkplugin.notifiers.data.XpNotificationData;
import dinkplugin.util.Utils;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Experience;
Expand All @@ -15,10 +16,14 @@
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.StatChanged;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.util.QuantityFormatter;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -38,7 +43,9 @@ public class LevelNotifier extends BaseNotifier {
private static final String COMBAT_NAME = "Combat";
private static final Set<String> COMBAT_COMPONENTS;
private final BlockingQueue<String> levelledSkills = new ArrayBlockingQueue<>(Skill.values().length + 1);
private final Collection<Skill> xpReached = EnumSet.noneOf(Skill.class);
private final Map<String, Integer> currentLevels = new HashMap<>();
private final Map<Skill, Integer> currentXp = new EnumMap<>(Skill.class);
private final AtomicInteger ticksWaited = new AtomicInteger();
private final AtomicInteger initTicks = new AtomicInteger();
private final AtomicBoolean shouldInit = new AtomicBoolean();
Expand All @@ -53,11 +60,13 @@ protected String getWebhookUrl() {

private void initLevels() {
for (Skill skill : Skill.values()) {
int xp = client.getSkillExperience(skill);
int level = client.getRealSkillLevel(skill); // O(1)
if (level >= MAX_REAL_LEVEL) {
level = getLevel(client.getSkillExperience(skill));
level = getLevel(xp);
}
currentLevels.put(skill.getName(), level);
currentXp.put(skill, xp);
}
currentLevels.put(COMBAT_NAME, calculateCombatLevel());
initTicks.set(0);
Expand All @@ -69,7 +78,9 @@ public void reset() {
initTicks.set(0);
levelledSkills.clear();
ticksWaited.set(0);
clientThread.invoke(xpReached::clear);
clientThread.invoke(currentLevels::clear);
clientThread.invokeLater(currentXp::clear);
}

public void onTick() {
Expand All @@ -83,7 +94,7 @@ public void onTick() {
return;
}

if (levelledSkills.isEmpty()) {
if (levelledSkills.isEmpty() && xpReached.isEmpty()) {
return;
}

Expand All @@ -101,7 +112,7 @@ public void onTick() {
}

public void onStatChanged(StatChanged statChange) {
this.handleLevelUp(statChange.getSkill().getName(), statChange.getLevel(), statChange.getXp());
this.handleLevelUp(statChange.getSkill(), statChange.getLevel(), statChange.getXp());
}

public void onGameStateChanged(GameStateChanged gameStateChanged) {
Expand All @@ -110,25 +121,44 @@ public void onGameStateChanged(GameStateChanged gameStateChanged) {
}
}

private void handleLevelUp(String skill, int level, int xp) {
private void handleLevelUp(Skill skill, int level, int xp) {
if (!isEnabled()) return;

Integer previousXp = currentXp.put(skill, xp);
if (previousXp == null) {
shouldInit.set(true);
return;
}

String skillName = skill.getName();
int virtualLevel = level < MAX_REAL_LEVEL ? level : getLevel(xp); // avoid log(n) query when not needed
Integer previousLevel = currentLevels.put(skill, virtualLevel);
Integer previousLevel = currentLevels.put(skillName, virtualLevel);

if (previousLevel == null) {
shouldInit.set(true);
return;
}

if (virtualLevel < previousLevel) {
if (virtualLevel < previousLevel || xp < previousXp) {
// base skill level should never regress; reset notifier state
reset();
return;
}

// Check normal skill level up
checkLevelUp(config.notifyLevel(), skill, previousLevel, virtualLevel);
boolean enabled = config.notifyLevel();
checkLevelUp(enabled, skillName, previousLevel, virtualLevel);

// Check if xp milestone reached
int xpInterval = config.xpInterval();
if (enabled && xpInterval > 0 && (level >= MAX_REAL_LEVEL || !config.ignoreXpBelowMax())) {
int remainder = xp % xpInterval;
if (remainder == 0 || xp - remainder > previousXp) {
log.debug("Observed XP milestone for {} to {}", skill, xp);
xpReached.add(skill);
ticksWaited.set(0);
}
}

// Skip combat level checking if no level up has occurred
if (virtualLevel <= previousLevel) {
Expand All @@ -138,7 +168,7 @@ private void handleLevelUp(String skill, int level, int xp) {
}

// Check for combat level increase
if (COMBAT_COMPONENTS.contains(skill)) {
if (COMBAT_COMPONENTS.contains(skillName)) {
int combatLevel = calculateCombatLevel();
Integer previousCombatLevel = currentLevels.put(COMBAT_NAME, combatLevel);
checkLevelUp(config.notifyLevel() && config.levelNotifyCombat(), COMBAT_NAME, previousCombatLevel, combatLevel);
Expand Down Expand Up @@ -170,11 +200,60 @@ private void checkLevelUp(boolean configEnabled, String skill, Integer previousL
}

private void attemptNotify() {
notifyLevels();
notifyXp();
}

private void notifyXp() {
final int n = xpReached.size();
if (n == 0) return;

int interval = config.xpInterval();
Map<Skill, Integer> current = new EnumMap<>(currentXp);
List<Skill> milestones = new ArrayList<>(n);
JoiningReplacement.JoiningReplacementBuilder skillMessage = JoiningReplacement.builder().delimiter(", ");
for (Skill skill : xpReached) {
int xp = current.getOrDefault(skill, 0);
xp -= xp % interval;
milestones.add(skill);
skillMessage.component(
JoiningReplacement.builder()
.component(Replacements.ofWiki(skill.getName()))
.component(Replacements.ofText(String.format(" to %s XP", QuantityFormatter.formatNumber(xp))))
.build()
);
}
xpReached.clear();

String totalXp = QuantityFormatter.formatNumber(client.getOverallExperience());
String thumbnail = n == 1 ? getSkillIcon(milestones.get(0).getName()) : null;
Template fullNotification = Template.builder()
.template(config.levelNotifyMessage())
.replacementBoundary("%")
.replacement("%USERNAME%", Replacements.ofText(Utils.getPlayerName(client)))
.replacement("%SKILL%", skillMessage.build())
.replacement("%TOTAL_LEVEL%", Replacements.ofText(String.valueOf(client.getTotalLevel())))
.replacement("%TOTAL_XP%", Replacements.ofText(totalXp))
.build();

createMessage(config.levelSendImage(), NotificationBody.builder()
.text(fullNotification)
.extra(new XpNotificationData(current, milestones, interval, totalXp))
.type(NotificationType.XP)
.thumbnailUrl(thumbnail)
.build());
}

private void notifyLevels() {
int n = levelledSkills.size();
if (n == 0) return;

// Prepare level state
int totalLevel = client.getTotalLevel();
List<String> levelled = new ArrayList<>(levelledSkills.size());
levelledSkills.drainTo(levelled);
int count = levelled.size();
List<String> levelled = new ArrayList<>(n);
int count = levelledSkills.drainTo(levelled);
if (count == 0) return;

Map<String, Integer> lSkills = new HashMap<>(count);
Map<String, Integer> currentLevels = new HashMap<>(this.currentLevels);

Expand Down Expand Up @@ -232,6 +311,7 @@ private void attemptNotify() {
.replacement("%USERNAME%", Replacements.ofText(Utils.getPlayerName(client)))
.replacement("%SKILL%", skillMessage.build())
.replacement("%TOTAL_LEVEL%", Replacements.ofText(String.valueOf(totalLevel)))
.replacement("%TOTAL_XP%", Replacements.ofText(QuantityFormatter.formatNumber(client.getOverallExperience())))
.build();

// Fire notification
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/dinkplugin/notifiers/data/XpNotificationData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dinkplugin.notifiers.data;

import dinkplugin.message.Field;
import lombok.EqualsAndHashCode;
import lombok.Value;
import net.runelite.api.Experience;
import net.runelite.api.Skill;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

@Value
@EqualsAndHashCode(callSuper = false)
public class XpNotificationData extends NotificationData {
Map<Skill, Integer> xpData;
Collection<Skill> milestoneAchieved;
int interval;
transient String totalXp;

@Override
public List<Field> getFields() {
List<Field> fields = new ArrayList<>(2);
fields.add(new Field("Total XP", Field.formatBlock("", totalXp)));

SortedSet<String> maxed = new TreeSet<>();
for (var entry : xpData.entrySet()) {
if (entry.getValue() >= Experience.MAX_SKILL_XP) {
maxed.add(entry.getKey().getName());
}
}
if (!maxed.isEmpty()) {
fields.add(new Field("Maxed Skills", Field.formatBlock("", String.join(", ", maxed))));
}

return fields;
}
}
2 changes: 1 addition & 1 deletion src/test/java/dinkplugin/notifiers/LevelNotifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ void testNotifyJump() {
Map<String, Integer> expectedSkills = skillsMap("Hunter", 6);

// fire skill event (4 => 6, skipping 5 while 5 is level interval)
plugin.onStatChanged(new StatChanged(Skill.HUNTER, 200, 6, 6));
plugin.onStatChanged(new StatChanged(Skill.HUNTER, 600, 6, 6));

// let ticks pass
IntStream.range(0, 4).forEach(i -> notifier.onTick());
Expand Down

0 comments on commit 65f9ab2

Please sign in to comment.