Compare commits
24 Commits
cfc77b551c
...
renovate/c
Author | SHA1 | Date | |
---|---|---|---|
3eceec72b3 | |||
9771b04589 | |||
3a60a8050b | |||
8d1ef26183 | |||
d96a38a996 | |||
6c63534988 | |||
f2f45ffa87 | |||
0b5f083366 | |||
ddaa1e7c97 | |||
c0d2781fd0 | |||
e7ee0ef4af | |||
80331ba972 | |||
f5b8aa82c7 | |||
e5fbb3d44c | |||
54b20cf016 | |||
bc8a9f6fdc | |||
ae19233ddf | |||
2b8b135cf8 | |||
4cd6c27b2b | |||
b08961adf8 | |||
309a2211f4 | |||
9af387ca56 | |||
de572779e4 | |||
23814d8cfd |
12
pom.xml
12
pom.xml
@ -5,7 +5,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cc.fascinated</groupId>
|
||||
<artifactId>ScoreSaberUtils</artifactId>
|
||||
<artifactId>ScoreSaberUtils-Backend</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
@ -80,7 +80,15 @@
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sentry -->
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
|
||||
<version>7.8.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Websockets -->
|
||||
|
31
src/main/java/cc/fascinated/backend/common/MathUtils.java
Normal file
31
src/main/java/cc/fascinated/backend/common/MathUtils.java
Normal file
@ -0,0 +1,31 @@
|
||||
package cc.fascinated.backend.common;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class MathUtils {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
package cc.fascinated.backend.common;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class Timer {
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@ package cc.fascinated.backend.exception;
|
||||
|
||||
import cc.fascinated.backend.model.response.ErrorResponse;
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import io.sentry.Sentry;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
@ -39,6 +40,7 @@ public final class ExceptionControllerAdvice {
|
||||
}
|
||||
if (status == null) { // Fallback to 500
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
Sentry.captureException(ex); // Capture the exception
|
||||
}
|
||||
return new ResponseEntity<>(new ErrorResponse(status, message), status);
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package cc.fascinated.backend.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;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package cc.fascinated.backend.leaderboard;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter @AllArgsConstructor
|
||||
public class LeaderboardCurvePoint {
|
||||
|
||||
private final double a;
|
||||
private final double b;
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
package cc.fascinated.backend.leaderboard.impl;
|
||||
|
||||
import cc.fascinated.backend.common.MathUtils;
|
||||
import cc.fascinated.backend.leaderboard.Leaderboard;
|
||||
import cc.fascinated.backend.leaderboard.LeaderboardCurvePoint;
|
||||
import cc.fascinated.backend.model.score.Score;
|
||||
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<Score> 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<Score> 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<Score> scores, double expectedPP) {
|
||||
int left = 0;
|
||||
int right = scores.size() - 1;
|
||||
int boundaryIdx = -1;
|
||||
|
||||
while (left <= right) {
|
||||
int mid = (left + right) / 2;
|
||||
double bottomPP = this.getTotalPP(scores.subList(mid, scores.size()), mid);
|
||||
|
||||
List<Score> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,9 +8,14 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An account for ScoreSaber.
|
||||
*/
|
||||
@Document
|
||||
@AllArgsConstructor
|
||||
@Getter @Setter
|
||||
public class Account {
|
||||
@ -45,6 +50,11 @@ public class Account {
|
||||
*/
|
||||
private double performancePoints;
|
||||
|
||||
/**
|
||||
* The amount of raw pp to get 1 global performance point.
|
||||
*/
|
||||
private double rawPerGlobalPerformancePoints;
|
||||
|
||||
/**
|
||||
* The rank for this account.
|
||||
*/
|
||||
@ -117,6 +127,7 @@ public class Account {
|
||||
Bio.fromRaw(token.getBio()),
|
||||
token.getCountry(),
|
||||
token.getPp(),
|
||||
-1,
|
||||
token.getRank(),
|
||||
token.getCountryRank(),
|
||||
token.getRole(),
|
||||
@ -152,6 +163,9 @@ public class Account {
|
||||
* @return The bio.
|
||||
*/
|
||||
public static Bio fromRaw(String raw) {
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new Bio(
|
||||
raw.split("\n"),
|
||||
raw.replaceAll("<[^>]*>", "").split("\n")
|
||||
|
@ -5,12 +5,14 @@ import cc.fascinated.backend.model.score.Score;
|
||||
import cc.fascinated.backend.model.token.ScoreSaberLeaderboardToken;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A leaderboard for a song.
|
||||
*/
|
||||
@Document
|
||||
@AllArgsConstructor @Getter
|
||||
public class Leaderboard {
|
||||
/**
|
||||
|
@ -1,6 +1,7 @@
|
||||
package cc.fascinated.backend.model.score;
|
||||
|
||||
import cc.fascinated.backend.common.DateUtils;
|
||||
import cc.fascinated.backend.leaderboard.impl.ScoreSaberLeaderboard;
|
||||
import cc.fascinated.backend.model.token.ScoreSaberLeaderboardToken;
|
||||
import cc.fascinated.backend.model.token.ScoreSaberPlayerScoreToken;
|
||||
import cc.fascinated.backend.model.token.ScoreSaberScoreToken;
|
||||
@ -8,12 +9,17 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor @Getter @Setter
|
||||
/**
|
||||
* A score for an account.
|
||||
*/
|
||||
@Document
|
||||
@Getter @Setter
|
||||
public class Score {
|
||||
/**
|
||||
* The id for this score.
|
||||
@ -39,7 +45,7 @@ public class Score {
|
||||
/**
|
||||
* The PP for this score.
|
||||
*/
|
||||
private double pp;
|
||||
private Double pp;
|
||||
|
||||
/**
|
||||
* The weight for this score.
|
||||
@ -121,11 +127,45 @@ public class Score {
|
||||
*/
|
||||
private String leaderboardId;
|
||||
|
||||
/**
|
||||
* The curve version for this score.
|
||||
*/
|
||||
private Integer curveVersion;
|
||||
|
||||
/**
|
||||
* The difficulty this score was set on.
|
||||
*/
|
||||
private Difficulty difficulty;
|
||||
|
||||
public Score(String id, int rank, int baseScore, int modifiedScore, Double pp, int weight, List<String> modifiers,
|
||||
int multiplier, int badCuts, int missedNotes, int maxCombo, boolean fullCombo, int hmd, Date timeSet,
|
||||
boolean hasReplay, String deviceHmd, String deviceControllerLeft, String deviceControllerRight,
|
||||
List<Score> previousScores, String accountId, String leaderboardId, Integer curveVersion, Difficulty difficulty) {
|
||||
this.id = id;
|
||||
this.rank = rank;
|
||||
this.baseScore = baseScore;
|
||||
this.modifiedScore = modifiedScore;
|
||||
this.pp = pp;
|
||||
this.weight = weight;
|
||||
this.modifiers = modifiers;
|
||||
this.multiplier = multiplier;
|
||||
this.badCuts = badCuts;
|
||||
this.missedNotes = missedNotes;
|
||||
this.maxCombo = maxCombo;
|
||||
this.fullCombo = fullCombo;
|
||||
this.hmd = hmd;
|
||||
this.timeSet = timeSet;
|
||||
this.hasReplay = hasReplay;
|
||||
this.deviceHmd = deviceHmd;
|
||||
this.deviceControllerLeft = deviceControllerLeft;
|
||||
this.deviceControllerRight = deviceControllerRight;
|
||||
this.previousScores = previousScores;
|
||||
this.accountId = accountId;
|
||||
this.leaderboardId = leaderboardId;
|
||||
this.curveVersion = curveVersion == null ? ScoreSaberLeaderboard.INSTANCE.getCurveVersion() : curveVersion;
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a score from the given token.
|
||||
*
|
||||
@ -164,6 +204,7 @@ public class Score {
|
||||
new ArrayList<>(),
|
||||
playerId,
|
||||
leaderboard.getId(),
|
||||
ScoreSaberLeaderboard.INSTANCE.getCurveVersion(), // Get the curve version from the leaderboard.
|
||||
Difficulty.fromId(leaderboard.getDifficulty().getDifficulty())
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,16 @@ public interface ScoreRepository extends MongoRepository<Score, String> {
|
||||
* @return The scores for the account.
|
||||
*/
|
||||
@Query("{ 'accountId' : ?0 }")
|
||||
List<Score> getScoresForAccount(String accountId);
|
||||
List<Score> getScores(String accountId);
|
||||
|
||||
/**
|
||||
* Gets the ranked scores for an account.
|
||||
*
|
||||
* @param accountId The id of the account.
|
||||
* @return The ranked scores for the account.
|
||||
*/
|
||||
@Query(value = "{ 'accountId' : ?0, 'pp' : { $gt : 0 } }", sort = "{ 'pp' : -1 }")
|
||||
List<Score> getRankedScores(String accountId);
|
||||
|
||||
/**
|
||||
* Gets the scores sorted by the newest for an account.
|
||||
@ -28,4 +37,14 @@ public interface ScoreRepository extends MongoRepository<Score, String> {
|
||||
*/
|
||||
@Query(value = "{ 'accountId' : ?0 }", sort = "{ 'timeSet' : -1 }")
|
||||
List<Score> getScoresSortedByNewest(String accountId);
|
||||
|
||||
/**
|
||||
* Gets the scores for an account and a leaderboard.
|
||||
*
|
||||
* @param accountId The id of the account.
|
||||
* @param leaderboardId The id of the leaderboard.
|
||||
* @return The scores for the leaderboard.
|
||||
*/
|
||||
@Query(value = "{ 'accountId' : ?0, 'leaderboardId' : ?1 }", sort = "{ 'timeSet' : -1 }")
|
||||
List<Score> getScoreForLeaderboard(String accountId, String leaderboardId);
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package cc.fascinated.backend.service;
|
||||
|
||||
import cc.fascinated.backend.common.Timer;
|
||||
import cc.fascinated.backend.leaderboard.impl.ScoreSaberLeaderboard;
|
||||
import cc.fascinated.backend.model.account.Account;
|
||||
import cc.fascinated.backend.model.token.ScoreSaberAccountToken;
|
||||
import cc.fascinated.backend.repository.AccountRepository;
|
||||
import cc.fascinated.backend.repository.ScoreRepository;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -25,14 +27,20 @@ public class AccountService {
|
||||
*/
|
||||
private final AccountRepository accountRepository;
|
||||
|
||||
/**
|
||||
* The {@link ScoreRepository} instance.
|
||||
*/
|
||||
private final ScoreRepository scoreRepository;
|
||||
|
||||
/**
|
||||
* The {@link ScoreSaberService} instance.
|
||||
*/
|
||||
private final ScoreSaberService scoreSaberService;
|
||||
|
||||
@Autowired
|
||||
public AccountService(AccountRepository accountRepository, ScoreSaberService scoreSaberService) {
|
||||
public AccountService(AccountRepository accountRepository, ScoreRepository scoreRepository, ScoreSaberService scoreSaberService) {
|
||||
this.accountRepository = accountRepository;
|
||||
this.scoreRepository = scoreRepository;
|
||||
this.scoreSaberService = scoreSaberService;
|
||||
|
||||
Timer.scheduleRepeating(() -> {
|
||||
@ -62,11 +70,7 @@ public class AccountService {
|
||||
Optional<Account> optionalAccount = accountRepository.findById(id);
|
||||
if (optionalAccount.isEmpty()) {
|
||||
log.info("Account '{}' not found in the database. Fetching from ScoreSaber API.", id);
|
||||
|
||||
Account account = Account.fromToken(scoreSaberService.getAccount(id));
|
||||
updateAccount(account); // Fetch the scores for the account.
|
||||
accountRepository.save(account); // Save the account to the database.
|
||||
return account;
|
||||
return updateAccount(Account.fromToken(scoreSaberService.getAccount(id)));
|
||||
}
|
||||
log.info("Account '{}' found in the database.", id);
|
||||
return optionalAccount.get();
|
||||
@ -78,21 +82,31 @@ public class AccountService {
|
||||
*
|
||||
* @param account The account.
|
||||
*/
|
||||
public void updateAccount(Account account) {
|
||||
public Account updateAccount(Account account) {
|
||||
String id = account.getId();
|
||||
|
||||
// Fetch the account from the ScoreSaber API.
|
||||
ScoreSaberAccountToken accountToken = scoreSaberService.getAccount(id); // Fetch the account from the ScoreSaber API.
|
||||
if (accountToken == null) {
|
||||
log.warn("Account '{}' not found in the ScoreSaber API.", id);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the account with the new token.
|
||||
Account updatedAccount = Account.fromToken(accountToken);
|
||||
account = accountRepository.save(updatedAccount); // Save the account to the database.
|
||||
account = Account.fromToken(accountToken);
|
||||
accountRepository.save(account);
|
||||
|
||||
// Don't fetch new scores if the account is inactive or banned.
|
||||
if (account.isInactive() || account.isBanned()) {
|
||||
return account;
|
||||
}
|
||||
|
||||
// Fetch the scores for the account.
|
||||
scoreSaberService.updateScores(account);
|
||||
|
||||
// Set the raw pp per +1 global pp
|
||||
double rawPerGlobalPP = ScoreSaberLeaderboard.INSTANCE.getRawPerGlobalPP(scoreRepository.getRankedScores(id), 1);
|
||||
account.setRawPerGlobalPerformancePoints(rawPerGlobalPP);
|
||||
return accountRepository.save(account);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import cc.fascinated.backend.Main;
|
||||
import cc.fascinated.backend.common.DateUtils;
|
||||
import cc.fascinated.backend.common.Timer;
|
||||
import cc.fascinated.backend.common.WebRequest;
|
||||
import cc.fascinated.backend.exception.impl.BadRequestException;
|
||||
import cc.fascinated.backend.exception.impl.RateLimitException;
|
||||
import cc.fascinated.backend.exception.impl.ResourceNotFoundException;
|
||||
import cc.fascinated.backend.model.account.Account;
|
||||
@ -79,6 +80,12 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
||||
log.info("Account with id '{}' not found.", id);
|
||||
throw new ResourceNotFoundException("Account with id '%s' not found.".formatted(id));
|
||||
}
|
||||
if (account.isBanned()) {
|
||||
throw new BadRequestException("Account with id '%s' is banned.".formatted(id));
|
||||
}
|
||||
if (account.isInactive()) {
|
||||
throw new BadRequestException("Account with id '%s' is inactive.".formatted(id));
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
@ -90,7 +97,7 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
||||
* @return The scores.
|
||||
*/
|
||||
public ScoreSaberScoresPageToken getPageScores(Account account, int page) {
|
||||
log.info("Fetching scores for account '{}' from page {}.", account.getId(), page);
|
||||
log.info("Fetching scores for account '{}' from page {}.", account.getName(), page);
|
||||
ScoreSaberScoresPageToken pageToken = WebRequest.getAsEntity(String.format(GET_PLAYER_SCORES_ENDPOINT, account.getId(), "recent", page), ScoreSaberScoresPageToken.class);
|
||||
if (pageToken == null) { // Check if the page doesn't exist.
|
||||
return null;
|
||||
@ -120,54 +127,29 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
||||
return scores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the scores until the specified score id.
|
||||
*
|
||||
* @param account The account.
|
||||
* @param scoreUntil The score to fetch until.
|
||||
* @return The scores.
|
||||
*/
|
||||
public List<ScoreSaberPlayerScoreToken> getScoreUntil(Account account, Score scoreUntil) {
|
||||
List<ScoreSaberPlayerScoreToken> scores = new ArrayList<>();
|
||||
int page = 1;
|
||||
do {
|
||||
ScoreSaberScoresPageToken pageToken = getPageScores(account, page);
|
||||
for (ScoreSaberPlayerScoreToken score : pageToken.getPlayerScores()) {
|
||||
// If the score isn't the same as the scoreUntil, add it to the list.
|
||||
if (!DateUtils.getDateFromString(score.getScore().getTimeSet()).equals(scoreUntil.getTimeSet())) {
|
||||
scores.add(score);
|
||||
}
|
||||
|
||||
if (score.getScore().getId().equals(scoreUntil.getId())) {
|
||||
// If the current score matches the specified scoreUntil, stop fetching.
|
||||
return scores;
|
||||
}
|
||||
}
|
||||
page++;
|
||||
} while (true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the scores for the account.
|
||||
*
|
||||
* @param account The account.
|
||||
*/
|
||||
public void updateScores(Account account) {
|
||||
String id = account.getId();
|
||||
String name = account.getName();
|
||||
|
||||
// Fetch the scores for the account.
|
||||
List<Score> scores = scoreRepository.getScoresForAccount(id);
|
||||
List<Score> scores = scoreRepository.getScores(account.getId());
|
||||
if (scores.isEmpty()) {
|
||||
log.warn("Account '{}' has no scores, fetching them.", id);
|
||||
log.warn("Account '{}' has no scores, fetching them.", name);
|
||||
|
||||
List<ScoreSaberScoresPageToken> scoresPageTokens = this.getScores(account);
|
||||
List<Score> newScores = new ArrayList<>();
|
||||
List<Leaderboard> leaderboardToSave = new ArrayList<>();
|
||||
|
||||
for (ScoreSaberScoresPageToken page : scoresPageTokens) {
|
||||
for (ScoreSaberPlayerScoreToken score : page.getPlayerScores()) {
|
||||
newScores.add(Score.fromToken(id, score));
|
||||
leaderboardToSave.add(Leaderboard.fromToken(score.getLeaderboard()));
|
||||
for (ScoreSaberPlayerScoreToken scoreToken : page.getPlayerScores()) {
|
||||
Score score = Score.fromToken(account.getId(), scoreToken);
|
||||
newScores.add(score);
|
||||
scores.add(score);
|
||||
leaderboardToSave.add(Leaderboard.fromToken(scoreToken.getLeaderboard()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,39 +161,45 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
||||
}
|
||||
|
||||
scoreRepository.saveAll(newScores); // Save the player's scores.
|
||||
log.info("Found {} scores for account '{}'.", newScores.size(), id);
|
||||
log.info("Found {} scores for account '{}'.", newScores.size(), name);
|
||||
return;
|
||||
}
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
log.info("Fetching new scores for account '{}'.", id);
|
||||
Score latestScore = scoreRepository.getScoresSortedByNewest(id).get(0);
|
||||
log.info("Fetching new scores for account '{}'.", name);
|
||||
|
||||
List<ScoreSaberPlayerScoreToken> newScores = this.getScoreUntil(account, latestScore);
|
||||
if (newScores.isEmpty()) {
|
||||
log.info("No new scores found for account '{}'.", id);
|
||||
return;
|
||||
}
|
||||
|
||||
int newScoreCount = 0;
|
||||
for (ScoreSaberPlayerScoreToken newScore : newScores) {
|
||||
if (saveScore(account, newScore)) {
|
||||
newScoreCount++;
|
||||
int page = 1; // The current page to search for scores.
|
||||
boolean done = false; // Whether we are done fetching scores.
|
||||
List<ScoreSaberPlayerScoreToken> newScores = new ArrayList<>();
|
||||
do {
|
||||
// This will keep fetching score pages until it finds a score that already exists.
|
||||
ScoreSaberScoresPageToken pageScores = getPageScores(account, page);
|
||||
for (ScoreSaberPlayerScoreToken score : pageScores.getPlayerScores()) {
|
||||
boolean exists = scores.stream().anyMatch(s -> s.getId().equals(score.getScore().getId()));
|
||||
if (!exists) {
|
||||
newScores.add(score);
|
||||
continue;
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
page++;
|
||||
} while (!done);
|
||||
|
||||
// Save the new scores.
|
||||
for (ScoreSaberPlayerScoreToken score : newScores) {
|
||||
saveScore(account, score);
|
||||
}
|
||||
|
||||
log.info("Found {} new scores for account '{}'. (took: {}ms)", newScoreCount, id, System.currentTimeMillis() - start);
|
||||
log.info("Found {} new scores for account '{}'. (took: {}ms)", newScores.size(), name, System.currentTimeMillis() - start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the score for the account.
|
||||
*
|
||||
* @param account The account.
|
||||
* @param score The score to save.
|
||||
* @return Whether the score was saved.
|
||||
* @param score The score to save.
|
||||
*/
|
||||
private boolean saveScore(Account account, ScoreSaberPlayerScoreToken score) {
|
||||
boolean didSave = false;
|
||||
private void saveScore(Account account, ScoreSaberPlayerScoreToken score) {
|
||||
Leaderboard newScoreLeaderboard = Leaderboard.fromToken(score.getLeaderboard());
|
||||
Score oldScore = scoreRepository.findById(score.getScore().getId()).orElse(null);
|
||||
|
||||
@ -227,19 +215,16 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
||||
|
||||
scoreRepository.delete(oldScore); // Delete the old score.
|
||||
scoreRepository.save(scoreSet); // Save the new score.
|
||||
didSave = true;
|
||||
}
|
||||
} else {
|
||||
// The score is new
|
||||
scoreRepository.save(Score.fromToken(account.getId(), score)); // Save the new score.
|
||||
didSave = true;
|
||||
}
|
||||
|
||||
// Check if the leaderboard doesn't already exist.
|
||||
if (leaderboardRepository.findById(newScoreLeaderboard.getId()).isEmpty()) {
|
||||
leaderboardRepository.save(newScoreLeaderboard); // Save the leaderboard.
|
||||
}
|
||||
return didSave;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,9 +261,21 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void connectWebSocket() {
|
||||
log.info("Connecting to the ScoreSaber WSS.");
|
||||
new StandardWebSocketClient().execute(this, "wss://scoresaber.com/ws").get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(@NonNull WebSocketSession session) {
|
||||
log.info("Connected to the ScoreSaber WSS.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
|
||||
log.info("Disconnected from the ScoreSaber WSS.");
|
||||
connectWebSocket(); // Reconnect to the WebSocket.
|
||||
}
|
||||
|
||||
@Override @SneakyThrows
|
||||
protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) {
|
||||
// Ignore the connection message.
|
||||
@ -310,10 +307,4 @@ public class ScoreSaberService extends TextWebSocketHandler {
|
||||
log.error("An error occurred while handling the message.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
|
||||
log.info("Disconnected from the ScoreSaber WSS.");
|
||||
connectWebSocket(); // Reconnect to the WebSocket.
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,11 @@ spring:
|
||||
uri: mongodb://localhost:27017
|
||||
database: ssu-prod
|
||||
|
||||
# Sentry Configuration
|
||||
sentry:
|
||||
dsn: ""
|
||||
tracesSampleRate: 1.0
|
||||
|
||||
# Set the embedded MongoDB version
|
||||
de:
|
||||
flapdoodle:
|
||||
|
Reference in New Issue
Block a user