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(kc): include pb time in metadata for normal kills #648

Merged
merged 12 commits into from
Feb 6, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Unreleased

- Minor: Add `personalBest` time to killcount notification metadata. (#648)
- Minor: Fire loot notifications for new Royal Titans bosses. (#650)
- Minor: Include Bran pet name in pet notifications. (#649)
- Minor: Add `%SENDER%` template variable for chat notifier. (#644)
Expand Down
7 changes: 6 additions & 1 deletion docs/json-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,18 @@ JSON for Kill Count Notifications:
"gameMessage": "Your completed Chambers of Xeric count is: 69.",
"time": "PT46M34S",
"isPersonalBest": true,
"personalBest": null,
"party": ["%USERNAME%", "another RSN", "yet another RSN"]
},
"type": "KILL_COUNT"
}
```

When an associated duration is not found, `extra.time` and `extra.isPersonalBest` are not populated.
Both `extra.time` and `extra.personalBest` are reported in [ISO-8601 duration format](https://en.wikipedia.org/wiki/ISO_8601#Durations).

When an associated duration is not found, `extra.time`, `extra.isPersonalBest`, and `extra.personalBest` are not populated.
It is possible for both `extra.time` and `extra.isPersonalBest` to be populated while `extra.personalBest` is absent.
Also, `extra.personalBest` is never populated if `isPersonalBest` is true.

Note: when `boss` is `Penance Queen`, `count` refers to the high level gamble count, rather than kill count.

Expand Down
45 changes: 34 additions & 11 deletions src/main/java/dinkplugin/notifiers/KillCountNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import dinkplugin.util.KillCountService;
import dinkplugin.util.TimeUtils;
import dinkplugin.util.Utils;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.Varbits;
Expand All @@ -20,6 +21,7 @@
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.time.Duration;
import java.util.Optional;
Expand All @@ -40,7 +42,7 @@ public class KillCountNotifier extends BaseNotifier {

private static final Pattern PRIMARY_REGEX = Pattern.compile("Your (?<key>.+)\\s(?<type>kill|chest|completion|harvest)\\s?count is: ?(?<value>[\\d,]+)\\b", Pattern.CASE_INSENSITIVE);
private static final Pattern SECONDARY_REGEX = Pattern.compile("Your (?:completed|subdued) (?<key>.+) count is: (?<value>[\\d,]+)\\b");
private static final Pattern TIME_REGEX = Pattern.compile("(?:Duration|time|Subdued in):? (?<time>[\\d:]+(.\\d+)?)\\.?", Pattern.CASE_INSENSITIVE);
private static final Pattern TIME_REGEX = Pattern.compile("(?:Duration|time|Subdued in):? (?<time>[\\d:]+(?:.\\d+)?)\\.?(?: Personal best: (?<pbtime>[\\d:+]+(?:.\\d+)?))?", Pattern.CASE_INSENSITIVE);

private static final String BA_BOSS_NAME = "Penance Queen";

Expand All @@ -53,6 +55,9 @@ public class KillCountNotifier extends BaseNotifier {
@VisibleForTesting
static final int MAX_BAD_TICKS = 10;

@Inject
private KillCountService kcService;

private final AtomicInteger badTicks = new AtomicInteger();
private final AtomicReference<BossNotificationData> data = new AtomicReference<>();

Expand Down Expand Up @@ -92,7 +97,7 @@ public void onWidget(WidgetLoaded event) {
// https://oldschool.runescape.wiki/w/Barbarian_Assault/Rewards#Earning_Honour_points
if (widget != null && widget.getText().contains("80 ") && widget.getText().contains("5 ")) {
int gambleCount = client.getVarbitValue(Varbits.BA_GC);
this.data.set(new BossNotificationData(BA_BOSS_NAME, gambleCount, "The Queen is dead!", null, null, null));
this.data.set(new BossNotificationData(BA_BOSS_NAME, gambleCount, "The Queen is dead!", null, null, null, null));
}
}
}
Expand Down Expand Up @@ -121,12 +126,20 @@ private void handleKill(BossNotificationData data) {
return;

// ensure interval met or pb or ba, depending on config
boolean isPb = data.isPersonalBest() == Boolean.TRUE;
boolean ba = data.getBoss().equals(BA_BOSS_NAME);
if (!checkKillInterval(data.getCount(), data.isPersonalBest()) && !ba)
if (!checkKillInterval(data.getCount(), isPb) && !ba)
return;

// populate personalBest if absent
if (data.getPersonalBest() == null && !isPb) {
Duration pb = kcService.getPb(data.getBoss());
if (pb != null && (data.getTime() == null || pb.compareTo(data.getTime()) < 0)) {
data = data.withPersonalBest(pb);
}
}

// Assemble content
boolean isPb = data.isPersonalBest() == Boolean.TRUE;
String player = Utils.getPlayerName(client);
String time = TimeUtils.format(data.getTime(), TimeUtils.isPreciseTiming(client));
Template content = Template.builder()
Expand All @@ -147,8 +160,8 @@ private void handleKill(BossNotificationData data) {
.build());
}

private boolean checkKillInterval(int killCount, @Nullable Boolean pb) {
if (pb == Boolean.TRUE && config.killCountNotifyBestTime())
private boolean checkKillInterval(int killCount, boolean pb) {
if (pb && config.killCountNotifyBestTime())
return true;

if (killCount == 1 && config.killCountNotifyInitial())
Expand All @@ -174,6 +187,7 @@ private void updateData(BossNotificationData updated) {
defaultIfNull(updated.getGameMessage(), old.getGameMessage()),
updated.getTime() == null || (tob && old.getTime() != null) ? old.getTime() : updated.getTime(),
updated.isPersonalBest() == null || (tob && old.isPersonalBest() != null) ? old.isPersonalBest() : updated.isPersonalBest(),
updated.getPersonalBest() == null || (tob && old.getPersonalBest() != null) ? old.getPersonalBest() : updated.getPersonalBest(),
defaultIfNull(updated.getParty(), old.getParty())
);
}
Expand All @@ -184,21 +198,23 @@ private static Optional<BossNotificationData> parse(Client client, String messag
if (message.startsWith("Preparation")) return Optional.empty();
Optional<Pair<String, Integer>> boss = parseBoss(message);
if (boss.isPresent())
return boss.map(pair -> new BossNotificationData(pair.getLeft(), pair.getRight(), message, null, null, Utils.getBossParty(client, pair.getLeft())));
return boss.map(pair -> new BossNotificationData(pair.getLeft(), pair.getRight(), message, null, null, null, Utils.getBossParty(client, pair.getLeft())));

// TOB reports final wave duration before challenge time in the same message; skip to the part we care about
int tobIndex = message.startsWith("Wave") ? message.indexOf(KillCountService.TOB) : -1;
String msg = tobIndex < 0 ? message : message.substring(tobIndex);

return parseTime(msg).map(t -> new BossNotificationData(tobIndex < 0 ? null : KillCountService.TOB, null, null, t.getLeft(), t.getRight(), null));
return parseTime(msg).map(t -> new BossNotificationData(tobIndex < 0 ? null : KillCountService.TOB, null, null, t.getTime(), t.isPb(), t.getPb(), null));
}

private static Optional<Pair<Duration, Boolean>> parseTime(String message) {
private static Optional<ParsedTime> parseTime(String message) {
Matcher matcher = TIME_REGEX.matcher(message);
if (matcher.find()) {
Duration duration = TimeUtils.parseTime(matcher.group("time"));
boolean pb = message.toLowerCase().contains("(new personal best)");
return Optional.of(Pair.of(duration, pb));
boolean isPb = message.toLowerCase().contains("(new personal best)");
String pbTime = matcher.group("pbtime");
Duration pb = pbTime != null ? TimeUtils.parseTime(pbTime) : null;
return Optional.of(new ParsedTime(duration, isPb, pb));
}
return Optional.empty();
}
Expand Down Expand Up @@ -271,4 +287,11 @@ private static String parseSecondary(String boss) {

return null;
}

@Value
private static class ParsedTime {
Duration time;
boolean isPb;
@Nullable Duration pb;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import dinkplugin.util.DurationAdapter;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.With;
import lombok.experimental.Accessors;
import org.jetbrains.annotations.Nullable;

import java.time.Duration;
import java.util.Collection;
import java.util.List;

@With
@Value
@EqualsAndHashCode(callSuper = false)
public class BossNotificationData extends NotificationData {
Expand All @@ -23,6 +25,9 @@ public class BossNotificationData extends NotificationData {
@Accessors(fluent = true)
Boolean isPersonalBest;
@Nullable
@JsonAdapter(DurationAdapter.class)
Duration personalBest;
@Nullable
Collection<String> party;

@Override
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/dinkplugin/util/KillCountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import javax.inject.Inject;
import javax.inject.Singleton;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
Expand Down Expand Up @@ -207,6 +208,16 @@ private boolean isCorruptedGauntlet(LootReceived event) {
&& (CG_NAME.equals(lastDrop.getSource()) || CG_BOSS.equals(lastDrop.getSource()));
}

@Nullable
public Duration getPb(String boss) {
if (ConfigUtil.isPluginDisabled(configManager, RL_CHAT_CMD_PLUGIN_NAME)) return null;
Double pb = configManager.getRSProfileConfiguration("personalbest", cleanBossName(boss), double.class);
if (pb == null) return null;
int seconds = pb.intValue();
double millis = (pb - seconds) * 1000;
return Duration.ofSeconds(seconds).plusMillis((long) millis);
}

@Nullable
public Integer getKillCount(LootRecordType type, String sourceName) {
if (sourceName == null) return null;
Expand Down
Loading