update scoresaber me/user command to show raw per global
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m12s
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m12s
This commit is contained in:
parent
bef2b695f5
commit
bb81c098b2
@ -24,4 +24,28 @@ public final class MathUtils {
|
|||||||
).format(number)
|
).format(number)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps a value between a minimum and maximum.
|
||||||
|
*
|
||||||
|
* @param value The value to clamp.
|
||||||
|
* @param min The minimum value.
|
||||||
|
* @param max The maximum value.
|
||||||
|
* @return The clamped value.
|
||||||
|
*/
|
||||||
|
public static double clamp(double value, double min, double max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linearly interpolates between two values.
|
||||||
|
*
|
||||||
|
* @param a The first value.
|
||||||
|
* @param b The second value.
|
||||||
|
* @param t The interpolation value.
|
||||||
|
* @return The interpolated value.
|
||||||
|
*/
|
||||||
|
public static double lerp(double a, double b, double t) {
|
||||||
|
return a + t * (b - a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ public class NumberFormatter {
|
|||||||
* The suffixes for the numbers
|
* The suffixes for the numbers
|
||||||
*/
|
*/
|
||||||
private static final String[] SUFFIXES = new String[] { "K", "M", "B", "T", "Q", "QT", "S", "SP", "O", "N", "D", "UD", "DD", "TD" };
|
private static final String[] SUFFIXES = new String[] { "K", "M", "B", "T", "Q", "QT", "S", "SP", "O", "N", "D", "UD", "DD", "TD" };
|
||||||
private static final DecimalFormat FORMAT = new DecimalFormat("###.##");
|
private static final DecimalFormat FORMAT = new DecimalFormat("#,##0.##");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the provided double
|
* Format the provided double
|
||||||
@ -47,8 +47,8 @@ public class NumberFormatter {
|
|||||||
* @param input the value to format
|
* @param input the value to format
|
||||||
* @return the formatted double, in the format of xx,xxx,xxx
|
* @return the formatted double, in the format of xx,xxx,xxx
|
||||||
*/
|
*/
|
||||||
public static String formatCommas(double input) {
|
public static String simpleFormat(double input) {
|
||||||
return String.format("%,.0f", input);
|
return FORMAT.format(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package cc.fascinated.bat.common.beatsaber.leaderboard;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public class Leaderboard {
|
||||||
|
/**
|
||||||
|
* The name of the leaderboard.
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of the leaderboard.
|
||||||
|
*/
|
||||||
|
private int curveVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The curve of the leaderboard.
|
||||||
|
*/
|
||||||
|
private final LeaderboardCurvePoint[] curve;
|
||||||
|
}
|
10
src/main/java/cc/fascinated/bat/common/beatsaber/leaderboard/LeaderboardCurvePoint.java
Normal file
10
src/main/java/cc/fascinated/bat/common/beatsaber/leaderboard/LeaderboardCurvePoint.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package cc.fascinated.bat.common.beatsaber.leaderboard;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter @AllArgsConstructor
|
||||||
|
public class LeaderboardCurvePoint {
|
||||||
|
private final double a;
|
||||||
|
private final double b;
|
||||||
|
}
|
167
src/main/java/cc/fascinated/bat/common/beatsaber/leaderboard/impl/ScoreSaberLeaderboard.java
Normal file
167
src/main/java/cc/fascinated/bat/common/beatsaber/leaderboard/impl/ScoreSaberLeaderboard.java
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package cc.fascinated.bat.common.beatsaber.leaderboard.impl;
|
||||||
|
|
||||||
|
import cc.fascinated.bat.common.MathUtils;
|
||||||
|
import cc.fascinated.bat.common.beatsaber.leaderboard.Leaderboard;
|
||||||
|
import cc.fascinated.bat.common.beatsaber.leaderboard.LeaderboardCurvePoint;
|
||||||
|
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Log4j2(topic = "ScoreSaber Leaderboard")
|
||||||
|
public class ScoreSaberLeaderboard extends Leaderboard {
|
||||||
|
public static final ScoreSaberLeaderboard INSTANCE = new ScoreSaberLeaderboard();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base multiplier for stars.
|
||||||
|
*/
|
||||||
|
private final double starMultiplier = 42.11;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* no idea, ngl
|
||||||
|
*/
|
||||||
|
private final double weightCoefficient = 0.965;
|
||||||
|
|
||||||
|
public ScoreSaberLeaderboard() {
|
||||||
|
super("ScoreSaber", 1, new LeaderboardCurvePoint[] {
|
||||||
|
new LeaderboardCurvePoint(1.0, 5.367394282890631),
|
||||||
|
new LeaderboardCurvePoint(0.9995, 5.019543595874787),
|
||||||
|
new LeaderboardCurvePoint(0.999, 4.715470646416203),
|
||||||
|
new LeaderboardCurvePoint(0.99825, 4.325027383589547),
|
||||||
|
new LeaderboardCurvePoint(0.9975, 3.996793606763322),
|
||||||
|
new LeaderboardCurvePoint(0.99625, 3.5526145337555373),
|
||||||
|
new LeaderboardCurvePoint(0.995, 3.2022017597337955),
|
||||||
|
new LeaderboardCurvePoint(0.99375, 2.9190155639254955),
|
||||||
|
new LeaderboardCurvePoint(0.9925, 2.685667856592722),
|
||||||
|
new LeaderboardCurvePoint(0.99125, 2.4902905794106913),
|
||||||
|
new LeaderboardCurvePoint(0.99, 2.324506282149922),
|
||||||
|
new LeaderboardCurvePoint(0.9875, 2.058947159052738),
|
||||||
|
new LeaderboardCurvePoint(0.985, 1.8563887693647105),
|
||||||
|
new LeaderboardCurvePoint(0.9825, 1.697536248647543),
|
||||||
|
new LeaderboardCurvePoint(0.98, 1.5702410055532239),
|
||||||
|
new LeaderboardCurvePoint(0.9775, 1.4664726399289512),
|
||||||
|
new LeaderboardCurvePoint(0.975, 1.3807102743105126),
|
||||||
|
new LeaderboardCurvePoint(0.9725, 1.3090333065057616),
|
||||||
|
new LeaderboardCurvePoint(0.97, 1.2485807759957321),
|
||||||
|
new LeaderboardCurvePoint(0.965, 1.1552120359501035),
|
||||||
|
new LeaderboardCurvePoint(0.96, 1.0871883573850478),
|
||||||
|
new LeaderboardCurvePoint(0.955, 1.0388633331418984),
|
||||||
|
new LeaderboardCurvePoint(0.95, 1.0),
|
||||||
|
new LeaderboardCurvePoint(0.94, 0.9417362980580238),
|
||||||
|
new LeaderboardCurvePoint(0.93, 0.9039994071865736),
|
||||||
|
new LeaderboardCurvePoint(0.92, 0.8728710341448851),
|
||||||
|
new LeaderboardCurvePoint(0.91, 0.8488375988124467),
|
||||||
|
new LeaderboardCurvePoint(0.9, 0.825756123560842),
|
||||||
|
new LeaderboardCurvePoint(0.875, 0.7816934560296046),
|
||||||
|
new LeaderboardCurvePoint(0.85, 0.7462290664143185),
|
||||||
|
new LeaderboardCurvePoint(0.825, 0.7150465663454271),
|
||||||
|
new LeaderboardCurvePoint(0.8, 0.6872268862950283),
|
||||||
|
new LeaderboardCurvePoint(0.75, 0.6451808210101443),
|
||||||
|
new LeaderboardCurvePoint(0.7, 0.6125565959114954),
|
||||||
|
new LeaderboardCurvePoint(0.65, 0.5866010012767576),
|
||||||
|
new LeaderboardCurvePoint(0.6, 0.18223233667439062),
|
||||||
|
new LeaderboardCurvePoint(0.0, 0.0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the modifier for the given accuracy.
|
||||||
|
*
|
||||||
|
* @param accuracy The accuracy.
|
||||||
|
* @return The modifier.
|
||||||
|
*/
|
||||||
|
public double getModifier(double accuracy) {
|
||||||
|
accuracy = MathUtils.clamp(accuracy, 0, 100) / 100;
|
||||||
|
|
||||||
|
LeaderboardCurvePoint prev = this.getCurve()[1];
|
||||||
|
for (LeaderboardCurvePoint point : this.getCurve()) {
|
||||||
|
if (point.getA() <= accuracy) {
|
||||||
|
double distance = (prev.getA() - accuracy) / (prev.getA() - point.getA());
|
||||||
|
return MathUtils.lerp(prev.getB(), point.getB(), distance);
|
||||||
|
}
|
||||||
|
prev = point;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the pp for the given accuracy and stars.
|
||||||
|
*
|
||||||
|
* @param accuracy The accuracy.
|
||||||
|
* @param stars The stars.
|
||||||
|
* @return The pp.
|
||||||
|
*/
|
||||||
|
public double getPP(double accuracy, double stars) {
|
||||||
|
double pp = stars * this.starMultiplier;
|
||||||
|
double modifier = this.getModifier(accuracy);
|
||||||
|
return modifier * pp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total pp for the given scores.
|
||||||
|
*
|
||||||
|
* @param scores The scores.
|
||||||
|
* @return The total pp.
|
||||||
|
*/
|
||||||
|
private double getTotalPP(List<ScoreSaberScoreToken> scores, int startIdx) {
|
||||||
|
double totalPP = 0;
|
||||||
|
for (int i = 0; i < scores.size(); i++) {
|
||||||
|
totalPP += Math.pow(this.weightCoefficient, i + startIdx) * scores.get(i).getPp();
|
||||||
|
}
|
||||||
|
return totalPP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the pp at the given index for the given scores.
|
||||||
|
*
|
||||||
|
* @param bottomScores The scores.
|
||||||
|
* @param idx The index.
|
||||||
|
* @return The pp.
|
||||||
|
*/
|
||||||
|
private double getRawPPAtIdx(List<ScoreSaberScoreToken> bottomScores, int idx, double expected) {
|
||||||
|
double oldBottomPP = this.getTotalPP(bottomScores, idx);
|
||||||
|
double newBottomPP = this.getTotalPP(bottomScores, idx + 1);
|
||||||
|
|
||||||
|
return (expected + oldBottomPP - newBottomPP) / Math.pow(this.weightCoefficient, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the raw pp per global pp for the given scores.
|
||||||
|
*
|
||||||
|
* @param scores The scores.
|
||||||
|
* @param expectedPP The expected pp.
|
||||||
|
* @return The raw pp per global pp.
|
||||||
|
*/
|
||||||
|
public double getRawPerGlobalPP(List<ScoreSaberScoreToken> scores, double expectedPP) {
|
||||||
|
int left = 0;
|
||||||
|
int right = scores.size() - 1;
|
||||||
|
int boundaryIdx = -1;
|
||||||
|
|
||||||
|
// Sort by PP
|
||||||
|
scores.sort((a, b) -> Double.compare(b.getPp(), a.getPp()));
|
||||||
|
|
||||||
|
while (left <= right) {
|
||||||
|
int mid = (left + right) / 2;
|
||||||
|
double bottomPP = this.getTotalPP(scores.subList(mid, scores.size()), mid);
|
||||||
|
|
||||||
|
List<ScoreSaberScoreToken> bottomSlice = new ArrayList<>(scores.subList(mid, scores.size()));
|
||||||
|
bottomSlice.add(0, scores.get(mid));
|
||||||
|
double modifiedBottomPP = this.getTotalPP(bottomSlice, mid);
|
||||||
|
double diff = modifiedBottomPP - bottomPP;
|
||||||
|
|
||||||
|
if (diff > expectedPP) {
|
||||||
|
boundaryIdx = mid;
|
||||||
|
left = mid + 1;
|
||||||
|
} else {
|
||||||
|
right = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boundaryIdx == -1) {
|
||||||
|
return this.getRawPPAtIdx(scores, 0, expectedPP);
|
||||||
|
} else {
|
||||||
|
return this.getRawPPAtIdx(scores.subList(boundaryIdx + 1, scores.size()), boundaryIdx + 1, expectedPP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package cc.fascinated.bat.features.base.commands.server;
|
package cc.fascinated.bat.features.base.commands.server;
|
||||||
|
|
||||||
import cc.fascinated.bat.Emojis;
|
|
||||||
import cc.fascinated.bat.command.BatCommand;
|
import cc.fascinated.bat.command.BatCommand;
|
||||||
import cc.fascinated.bat.command.CommandInfo;
|
import cc.fascinated.bat.command.CommandInfo;
|
||||||
import cc.fascinated.bat.common.EmbedUtils;
|
import cc.fascinated.bat.common.EmbedUtils;
|
||||||
@ -8,16 +7,12 @@ import cc.fascinated.bat.common.NumberFormatter;
|
|||||||
import cc.fascinated.bat.model.BatGuild;
|
import cc.fascinated.bat.model.BatGuild;
|
||||||
import cc.fascinated.bat.model.BatUser;
|
import cc.fascinated.bat.model.BatUser;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import net.dv8tion.jda.api.OnlineStatus;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Nick (okNick)
|
* @author Nick (okNick)
|
||||||
*/
|
*/
|
||||||
|
@ -70,7 +70,7 @@ public class RemoveSubCommand extends BatCommand {
|
|||||||
""".formatted(
|
""".formatted(
|
||||||
Emojis.CHECK_MARK_EMOJI,
|
Emojis.CHECK_MARK_EMOJI,
|
||||||
textChannel.getAsMention(),
|
textChannel.getAsMention(),
|
||||||
NumberFormatter.formatCommas(counterChannel.getCurrentCount())
|
NumberFormatter.simpleFormat(counterChannel.getCurrentCount())
|
||||||
))
|
))
|
||||||
.build()).queue();
|
.build()).queue();
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package cc.fascinated.bat.features.logging.listeners;
|
|||||||
|
|
||||||
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
|
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
|
||||||
import cc.fascinated.bat.common.EmbedUtils;
|
import cc.fascinated.bat.common.EmbedUtils;
|
||||||
import cc.fascinated.bat.common.HexColorUtils;
|
|
||||||
import cc.fascinated.bat.common.RoleUtils;
|
import cc.fascinated.bat.common.RoleUtils;
|
||||||
import cc.fascinated.bat.event.EventListener;
|
import cc.fascinated.bat.event.EventListener;
|
||||||
import cc.fascinated.bat.features.logging.LogFeature;
|
import cc.fascinated.bat.features.logging.LogFeature;
|
||||||
|
@ -45,7 +45,7 @@ public class NumberOneScoreFeedListener implements EventListener {
|
|||||||
log.info("A new #1 score has been set by {} on {} ({})!",
|
log.info("A new #1 score has been set by {} on {} ({})!",
|
||||||
player.getName(),
|
player.getName(),
|
||||||
leaderboard.getSongName(),
|
leaderboard.getSongName(),
|
||||||
"%s⭐".formatted(NumberFormatter.formatCommas(leaderboard.getStars()))
|
"%s⭐".formatted(NumberFormatter.simpleFormat(leaderboard.getStars()))
|
||||||
);
|
);
|
||||||
|
|
||||||
for (Guild guild : DiscordService.JDA.getGuilds()) {
|
for (Guild guild : DiscordService.JDA.getGuilds()) {
|
||||||
|
@ -55,10 +55,10 @@ public class ScoreSaberFeature extends Feature {
|
|||||||
);
|
);
|
||||||
|
|
||||||
String accuracy = leaderboardToken.getMaxScore() == 0 ? "N/A" :
|
String accuracy = leaderboardToken.getMaxScore() == 0 ? "N/A" :
|
||||||
String.format("%s%%", NumberFormatter.formatCommas(((double) scoreToken.getBaseScore() / leaderboardToken.getMaxScore()) * 100));
|
String.format("%s%%", NumberFormatter.simpleFormat(((double) scoreToken.getBaseScore() / leaderboardToken.getMaxScore()) * 100));
|
||||||
|
|
||||||
String rawPp = scoreToken.getPp() == 0 ? "Unranked" : NumberFormatter.formatCommas(scoreToken.getPp());
|
String rawPp = scoreToken.getPp() == 0 ? "Unranked" : NumberFormatter.simpleFormat(scoreToken.getPp());
|
||||||
String rank = String.format("#%s", NumberFormatter.formatCommas(scoreToken.getRank()));
|
String rank = String.format("#%s", NumberFormatter.simpleFormat(scoreToken.getRank()));
|
||||||
String misses = String.format("%s", scoreToken.getMissedNotes());
|
String misses = String.format("%s", scoreToken.getMissedNotes());
|
||||||
String badCuts = String.format("%s", scoreToken.getBadCuts());
|
String badCuts = String.format("%s", scoreToken.getBadCuts());
|
||||||
String maxCombo = String.format("%s %s",
|
String maxCombo = String.format("%s %s",
|
||||||
|
@ -3,24 +3,17 @@ package cc.fascinated.bat.features.scoresaber.command.scoresaber;
|
|||||||
import cc.fascinated.bat.command.BatCommand;
|
import cc.fascinated.bat.command.BatCommand;
|
||||||
import cc.fascinated.bat.command.Category;
|
import cc.fascinated.bat.command.Category;
|
||||||
import cc.fascinated.bat.command.CommandInfo;
|
import cc.fascinated.bat.command.CommandInfo;
|
||||||
import cc.fascinated.bat.common.Colors;
|
import cc.fascinated.bat.common.*;
|
||||||
import cc.fascinated.bat.common.DateUtils;
|
|
||||||
import cc.fascinated.bat.common.EmbedUtils;
|
|
||||||
import cc.fascinated.bat.common.NumberFormatter;
|
|
||||||
import cc.fascinated.bat.exception.RateLimitException;
|
|
||||||
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
|
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
|
||||||
import cc.fascinated.bat.model.BatUser;
|
import cc.fascinated.bat.model.BatUser;
|
||||||
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken;
|
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken;
|
||||||
import cc.fascinated.bat.service.ScoreSaberService;
|
import cc.fascinated.bat.service.ScoreSaberService;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@ -42,19 +35,19 @@ public class ScoreSaberCommand extends BatCommand {
|
|||||||
*
|
*
|
||||||
* @param user The user to build the profile embed for
|
* @param user The user to build the profile embed for
|
||||||
* @param scoreSaberService The ScoreSaber service
|
* @param scoreSaberService The ScoreSaber service
|
||||||
* @param interaction The interaction
|
* @param event The interaction
|
||||||
*/
|
*/
|
||||||
public static void sendProfileEmbed(boolean isSelf, BatUser user, ScoreSaberService scoreSaberService, SlashCommandInteraction interaction) {
|
public static void sendProfileEmbed(boolean isSelf, BatUser user, ScoreSaberService scoreSaberService, SlashCommandInteraction event) {
|
||||||
ScoreSaberProfile profile = user.getScoreSaberProfile();
|
ScoreSaberProfile profile = user.getScoreSaberProfile();
|
||||||
if (profile.getAccountId() == null) {
|
if (profile.getAccountId() == null) {
|
||||||
if (!isSelf) {
|
if (!isSelf) {
|
||||||
interaction.replyEmbeds(EmbedUtils.errorEmbed()
|
event.replyEmbeds(EmbedUtils.errorEmbed()
|
||||||
.setDescription("%s does not have a linked ScoreSaber account".formatted(user.getDiscordUser().getAsMention()))
|
.setDescription("%s does not have a linked ScoreSaber account".formatted(user.getDiscordUser().getAsMention()))
|
||||||
.build())
|
.build())
|
||||||
.setEphemeral(true)
|
.setEphemeral(true)
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
interaction.replyEmbeds(EmbedUtils.errorEmbed()
|
event.replyEmbeds(EmbedUtils.errorEmbed()
|
||||||
.setDescription("You do not have a linked ScoreSaber account")
|
.setDescription("You do not have a linked ScoreSaber account")
|
||||||
.build())
|
.build())
|
||||||
.setEphemeral(true)
|
.setEphemeral(true)
|
||||||
@ -62,47 +55,89 @@ public class ScoreSaberCommand extends BatCommand {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
long before = System.currentTimeMillis();
|
||||||
long before = System.currentTimeMillis();
|
ScoreSaberAccountToken account = scoreSaberService.getAccount(profile.getAccountId());
|
||||||
boolean cached = scoreSaberService.isCached(profile.getAccountId());
|
if (account == null) {
|
||||||
ScoreSaberAccountToken account = scoreSaberService.getAccount(profile.getAccountId());
|
if (!isSelf) {
|
||||||
if (account == null) {
|
event.replyEmbeds(EmbedUtils.errorEmbed()
|
||||||
if (!isSelf) {
|
.setDescription("%s has an invalid ScoreSaber account linked, please ask them to re-link their account"
|
||||||
interaction.replyEmbeds(EmbedUtils.errorEmbed()
|
.formatted(user.getDiscordUser().getAsMention()))
|
||||||
.setDescription("%s has an invalid ScoreSaber account linked, please ask them to re-link their account"
|
|
||||||
.formatted(user.getDiscordUser().getAsMention()))
|
|
||||||
.build())
|
|
||||||
.setEphemeral(true)
|
|
||||||
.queue();
|
|
||||||
}
|
|
||||||
interaction.replyEmbeds(EmbedUtils.errorEmbed()
|
|
||||||
.setDescription("You have an invalid ScoreSaber account linked, please re-link your account")
|
|
||||||
.build())
|
.build())
|
||||||
.setEphemeral(true)
|
.setEphemeral(true)
|
||||||
.queue();
|
.queue();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
event.replyEmbeds(EmbedUtils.errorEmbed()
|
||||||
long fetchTime = System.currentTimeMillis() - before;
|
.setDescription("You have an invalid ScoreSaber account linked, please re-link your account")
|
||||||
interaction.replyEmbeds(new EmbedBuilder()
|
|
||||||
.setAuthor(account.getName() + "'s Profile", "https://scoresaber.com/u/%s".formatted(account.getId()),
|
|
||||||
"https://cdn.scoresaber.com/avatars/%s.jpg".formatted(account.getId()))
|
|
||||||
.addField("Name", account.getName(), true)
|
|
||||||
.addField("Country", account.getCountry(), true)
|
|
||||||
.addField("Rank", "#" + NumberFormatter.formatCommas(account.getRank()), true)
|
|
||||||
.addField("Country Rank", "#" + NumberFormatter.formatCommas(account.getCountryRank()), true)
|
|
||||||
.addField("PP", NumberFormatter.formatCommas(account.getPp()), true)
|
|
||||||
.addField("Joined", "<t:%s>".formatted(DateUtils.getDateFromString(account.getFirstSeen()).toInstant().getEpochSecond()), true)
|
|
||||||
.setTimestamp(LocalDateTime.now())
|
|
||||||
.setFooter(!cached ? "Fetched in %sms".formatted(fetchTime) : "Cached", "https://flagcdn.com/h120/%s.png".formatted(account.getCountry().toLowerCase()))
|
|
||||||
.setColor(Colors.DEFAULT)
|
|
||||||
.build()).queue();
|
|
||||||
} catch (RateLimitException ex) {
|
|
||||||
interaction.replyEmbeds(EmbedUtils.errorEmbed()
|
|
||||||
.setDescription("The ScoreSaber API is currently rate our limiting requests, please try again later")
|
|
||||||
.build())
|
.build())
|
||||||
.setEphemeral(true)
|
.setEphemeral(true)
|
||||||
.queue();
|
.queue();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile.getAccountId() == null) {
|
||||||
|
if (!isSelf) {
|
||||||
|
event.replyEmbeds(EmbedUtils.errorEmbed()
|
||||||
|
.setDescription("%s does not have a linked ScoreSaber account".formatted(user.getDiscordUser().getAsMention()))
|
||||||
|
.build())
|
||||||
|
.setEphemeral(true)
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
event.replyEmbeds(EmbedUtils.errorEmbed()
|
||||||
|
.setDescription("You do not have a linked ScoreSaber account")
|
||||||
|
.build())
|
||||||
|
.setEphemeral(true)
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.replyEmbeds(EmbedUtils.genericEmbed()
|
||||||
|
.setDescription("Loading profile for %s...".formatted(user.getDiscordUser().getAsMention()))
|
||||||
|
.build()).queue(message -> {
|
||||||
|
ScoreSaberService.RawPerGlobal rawPerGlobal = scoreSaberService.getRawPerGlobal(profile, (callback) -> {
|
||||||
|
int currentPage = callback.getCurrentPage();
|
||||||
|
int totalPages = callback.getTotalPages();
|
||||||
|
// Only update every 5 pages, but show the first page
|
||||||
|
if (currentPage % 5 != 0 && currentPage != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.editOriginalEmbeds(EmbedUtils.genericEmbed()
|
||||||
|
.setDescription("""
|
||||||
|
Loading profile for %s...
|
||||||
|
Page `%s`/`%s`
|
||||||
|
""".formatted(
|
||||||
|
user.getDiscordUser().getAsMention(),
|
||||||
|
currentPage,
|
||||||
|
totalPages
|
||||||
|
))
|
||||||
|
.build()).queue();
|
||||||
|
});
|
||||||
|
|
||||||
|
String name = account.getName();
|
||||||
|
String country = account.getCountry();
|
||||||
|
String rank = NumberFormatter.simpleFormat(account.getRank());
|
||||||
|
String countryRank = NumberFormatter.simpleFormat(account.getCountryRank());
|
||||||
|
String pp = NumberFormatter.simpleFormat(account.getPp());
|
||||||
|
String ppPerRawGlobal = NumberFormatter.simpleFormat(rawPerGlobal.getRawPerGlobal());
|
||||||
|
long joinedTimeInSeconds = DateUtils.getDateFromString(account.getFirstSeen()).toInstant().getEpochSecond();
|
||||||
|
String timeTaken = TimeUtils.format(System.currentTimeMillis() - before, TimeUtils.BatTimeFormat.FIT, true);
|
||||||
|
|
||||||
|
message.editOriginalEmbeds(EmbedUtils.successEmbed()
|
||||||
|
.setDescription(new EmbedDescriptionBuilder("%s's ScoreSaber Account".formatted(user.getDiscordUser().getAsMention()))
|
||||||
|
.appendLine("Name: `%s`".formatted(name), true)
|
||||||
|
.appendLine("Country: `%s`".formatted(country), true)
|
||||||
|
.appendLine("Rank: `#%s`".formatted(rank), true)
|
||||||
|
.appendLine("Country Rank: `#%s`".formatted(countryRank), true)
|
||||||
|
.appendLine("PP: `%spp`".formatted(pp), true)
|
||||||
|
.appendLine("Raw PP Per Global: `%spp`".formatted(ppPerRawGlobal), true)
|
||||||
|
.appendLine("Joined: <t:%s>".formatted(joinedTimeInSeconds), true)
|
||||||
|
.build())
|
||||||
|
.setThumbnail(account.getProfilePicture())
|
||||||
|
.setFooter("Took %s (%s/%s cached pages)".formatted(
|
||||||
|
timeTaken,
|
||||||
|
rawPerGlobal.getCachedPages(),
|
||||||
|
rawPerGlobal.getTotalPages()
|
||||||
|
), null)
|
||||||
|
.build()).queue();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,14 @@ package cc.fascinated.bat.service;
|
|||||||
import cc.fascinated.bat.BatApplication;
|
import cc.fascinated.bat.BatApplication;
|
||||||
import cc.fascinated.bat.common.DateUtils;
|
import cc.fascinated.bat.common.DateUtils;
|
||||||
import cc.fascinated.bat.common.WebRequest;
|
import cc.fascinated.bat.common.WebRequest;
|
||||||
|
import cc.fascinated.bat.common.beatsaber.leaderboard.impl.ScoreSaberLeaderboard;
|
||||||
import cc.fascinated.bat.event.EventListener;
|
import cc.fascinated.bat.event.EventListener;
|
||||||
import cc.fascinated.bat.exception.BadRequestException;
|
import cc.fascinated.bat.exception.BadRequestException;
|
||||||
import cc.fascinated.bat.exception.ResourceNotFoundException;
|
import cc.fascinated.bat.exception.ResourceNotFoundException;
|
||||||
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
|
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
|
||||||
import cc.fascinated.bat.model.token.beatsaber.scoresaber.*;
|
import cc.fascinated.bat.model.token.beatsaber.scoresaber.*;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import lombok.NonNull;
|
import lombok.*;
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import net.jodah.expiringmap.ExpiringMap;
|
import net.jodah.expiringmap.ExpiringMap;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -27,6 +27,7 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Log4j2(topic = "ScoreSaber Service")
|
@Log4j2(topic = "ScoreSaber Service")
|
||||||
@ -43,6 +44,13 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
|||||||
.expiration(5, TimeUnit.MINUTES)
|
.expiration(5, TimeUnit.MINUTES)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cached score pages.
|
||||||
|
*/
|
||||||
|
private final Map<String, CachedPage> cachedScorePages = ExpiringMap.builder()
|
||||||
|
.expiration(30, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ScoreSaberService() {
|
public ScoreSaberService() {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
@ -93,7 +101,15 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
|||||||
* @param page The page to get the scores from.
|
* @param page The page to get the scores from.
|
||||||
* @return The scores.
|
* @return The scores.
|
||||||
*/
|
*/
|
||||||
public ScoreSaberScoresPageToken getPageScores(ScoreSaberProfile profile, int page) {
|
public CachedPage getPageScores(ScoreSaberProfile profile, int page) {
|
||||||
|
String key = profile.getAccountId() + "." + page;
|
||||||
|
// Check if the page is cached, but don't cache the first page as it's the most likely to change.
|
||||||
|
if (cachedScorePages.containsKey(key) && page != 1) {
|
||||||
|
CachedPage cachedPage = cachedScorePages.get(key);
|
||||||
|
cachedPage.setCached(true);
|
||||||
|
return cachedPage;
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Fetching scores for account '{}' from page {}.", profile.getAccountId(), page);
|
log.info("Fetching scores for account '{}' from page {}.", profile.getAccountId(), page);
|
||||||
ScoreSaberScoresPageToken pageToken = WebRequest.getAsEntity(String.format(GET_PLAYER_SCORES_ENDPOINT, profile.getAccountId(), "recent", page), ScoreSaberScoresPageToken.class);
|
ScoreSaberScoresPageToken pageToken = WebRequest.getAsEntity(String.format(GET_PLAYER_SCORES_ENDPOINT, profile.getAccountId(), "recent", page), ScoreSaberScoresPageToken.class);
|
||||||
if (pageToken == null) { // Check if the page doesn't exist.
|
if (pageToken == null) { // Check if the page doesn't exist.
|
||||||
@ -103,7 +119,9 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
|||||||
pageToken.setPlayerScores(Arrays.stream(pageToken.getPlayerScores())
|
pageToken.setPlayerScores(Arrays.stream(pageToken.getPlayerScores())
|
||||||
.sorted((a, b) -> DateUtils.getDateFromString(b.getScore().getTimeSet()).compareTo(DateUtils.getDateFromString(a.getScore().getTimeSet())))
|
.sorted((a, b) -> DateUtils.getDateFromString(b.getScore().getTimeSet()).compareTo(DateUtils.getDateFromString(a.getScore().getTimeSet())))
|
||||||
.toArray(ScoreSaberPlayerScoreToken[]::new));
|
.toArray(ScoreSaberPlayerScoreToken[]::new));
|
||||||
return pageToken;
|
CachedPage cachedPage = new CachedPage(pageToken, false);
|
||||||
|
cachedScorePages.put(key, cachedPage);
|
||||||
|
return cachedPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,18 +130,39 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
|||||||
* @param profile The profile.
|
* @param profile The profile.
|
||||||
* @return The scores.
|
* @return The scores.
|
||||||
*/
|
*/
|
||||||
public List<ScoreSaberScoresPageToken> getScores(ScoreSaberProfile profile) {
|
public List<CachedPage> getScores(ScoreSaberProfile profile, Consumer<CurrentPageCallback> currentPageCallback) {
|
||||||
List<ScoreSaberScoresPageToken> scores = new ArrayList<>(List.of(getPageScores(profile, 1)));
|
List<CachedPage> scores = new ArrayList<>(List.of(getPageScores(profile, 1)));
|
||||||
ScoreSaberPageMetadataToken metadata = scores.get(0).getMetadata();
|
ScoreSaberPageMetadataToken metadata = scores.get(0).getPage().getMetadata();
|
||||||
int totalPages = (int) Math.ceil((double) metadata.getTotal() / metadata.getItemsPerPage());
|
int totalPages = (int) Math.ceil((double) metadata.getTotal() / metadata.getItemsPerPage());
|
||||||
log.info("Fetching {} pages of scores for account '{}'.", totalPages, profile.getAccountId());
|
log.info("Fetching {} pages of scores for account '{}'.", totalPages, profile.getAccountId());
|
||||||
for (int i = 2; i <= totalPages; i++) {
|
for (int i = 2; i <= totalPages; i++) {
|
||||||
scores.add(getPageScores(profile, i));
|
scores.add(getPageScores(profile, i));
|
||||||
|
currentPageCallback.accept(new CurrentPageCallback(i, totalPages));
|
||||||
}
|
}
|
||||||
|
|
||||||
return scores;
|
return scores;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the raw pp per global pp for the given account.
|
||||||
|
*
|
||||||
|
* @param account The account.
|
||||||
|
* @return The raw pp per global pp.
|
||||||
|
*/
|
||||||
|
public RawPerGlobal getRawPerGlobal(ScoreSaberProfile account, Consumer<CurrentPageCallback> currentPageCallback) {
|
||||||
|
List<CachedPage> scores = getScores(account, currentPageCallback);
|
||||||
|
List<ScoreSaberScoreToken> playerScores = new ArrayList<>();
|
||||||
|
for (CachedPage score : scores) {
|
||||||
|
for (ScoreSaberPlayerScoreToken playerScore : score.getPage().getPlayerScores()) {
|
||||||
|
playerScores.add(playerScore.getScore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RawPerGlobal(
|
||||||
|
ScoreSaberLeaderboard.INSTANCE.getRawPerGlobalPP(playerScores, 1),
|
||||||
|
(int) scores.stream().filter(CachedPage::isCached).count(),
|
||||||
|
scores.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to the ScoreSaber WebSocket.
|
* Connects to the ScoreSaber WebSocket.
|
||||||
*/
|
*/
|
||||||
@ -169,4 +208,45 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
|||||||
log.error("An error occurred while handling the message.", ex);
|
log.error("An error occurred while handling the message.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public static class CurrentPageCallback {
|
||||||
|
private int currentPage;
|
||||||
|
private int totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class CachedPage {
|
||||||
|
/**
|
||||||
|
* The page of scores.
|
||||||
|
*/
|
||||||
|
private ScoreSaberScoresPageToken page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the page is cached.
|
||||||
|
*/
|
||||||
|
private boolean cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public static class RawPerGlobal {
|
||||||
|
/**
|
||||||
|
* The raw pp per global pp.
|
||||||
|
*/
|
||||||
|
private double rawPerGlobal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of pages that were cached.
|
||||||
|
*/
|
||||||
|
private int cachedPages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amount of pages.
|
||||||
|
*/
|
||||||
|
private int totalPages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user