add support for changing curves
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m20s
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m20s
This commit is contained in:
parent
ee626a21bc
commit
66b4b18793
34
src/main/java/cc/fascinated/common/MathUtils.java
Normal file
34
src/main/java/cc/fascinated/common/MathUtils.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package cc.fascinated.common;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fascinated (fascinated7)
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,19 @@ package cc.fascinated.model.score;
|
|||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "score")
|
||||||
public class TrackedScore {
|
public class TrackedScore {
|
||||||
/**
|
/**
|
||||||
* The ID of the score.
|
* The ID of the score.
|
||||||
@ -86,6 +92,16 @@ public class TrackedScore {
|
|||||||
*/
|
*/
|
||||||
private String difficulty;
|
private String difficulty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The star count of the difficulty.
|
||||||
|
*/
|
||||||
|
private Double stars;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the score.
|
||||||
|
*/
|
||||||
|
private Date timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the Tracked Score as a DTO
|
* Gets the Tracked Score as a DTO
|
||||||
*/
|
*/
|
||||||
|
@ -2,14 +2,19 @@ package cc.fascinated.model.token;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Getter @ToString
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@Document("scoresaber_leaderboard")
|
||||||
public class ScoreSaberLeaderboardToken {
|
public class ScoreSaberLeaderboardToken {
|
||||||
/**
|
/**
|
||||||
* The ID of the leaderboard.
|
* The ID of the leaderboard.
|
||||||
*/
|
*/
|
||||||
|
@Id
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
21
src/main/java/cc/fascinated/platform/CurvePoint.java
Normal file
21
src/main/java/cc/fascinated/platform/CurvePoint.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package cc.fascinated.platform;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fascinated (fascinated7)
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class CurvePoint {
|
||||||
|
/**
|
||||||
|
* The x value of the curve point.
|
||||||
|
*/
|
||||||
|
private final double x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The y value of the curve point.
|
||||||
|
*/
|
||||||
|
private final double y;
|
||||||
|
}
|
@ -3,18 +3,61 @@ package cc.fascinated.platform;
|
|||||||
import cc.fascinated.exception.impl.BadRequestException;
|
import cc.fascinated.exception.impl.BadRequestException;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
public abstract class Platform {
|
public abstract class Platform {
|
||||||
/**
|
/**
|
||||||
* The name of the platform.
|
* The name of the platform.
|
||||||
*/
|
*/
|
||||||
private final Platforms platform;
|
private final Platforms platform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The previous curve version for this
|
||||||
|
* platform in the database.
|
||||||
|
*/
|
||||||
|
private int previousCurveVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current curve version for getting
|
||||||
|
* pp from a star count.
|
||||||
|
*/
|
||||||
|
private final int currentCurveVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The curve points for each curve version.
|
||||||
|
*/
|
||||||
|
private final Map<Integer, CurvePoint[]> curvePoints;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the curve version to see if it exists.
|
||||||
|
*
|
||||||
|
* @param curveVersion the curve version to check
|
||||||
|
* @throws BadRequestException if the curve version does not exist
|
||||||
|
*/
|
||||||
|
public void checkCurveVersion(int curveVersion) {
|
||||||
|
if (!curvePoints.containsKey(curveVersion)) {
|
||||||
|
throw new BadRequestException("Curve version '%s' for platform '%s' was not found.".formatted(curveVersion, platform.getPlatformName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the PP amount from the star count.
|
||||||
|
*
|
||||||
|
* @param stars the amount of stars
|
||||||
|
* @return the pp amount
|
||||||
|
*/
|
||||||
|
public abstract double getPp(int curveVersion, double stars, double accuracy);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every 10 minutes to update
|
* Called every 10 minutes to update
|
||||||
* the players data in QuestDB.
|
* the players data in QuestDB.
|
||||||
@ -27,6 +70,12 @@ public abstract class Platform {
|
|||||||
*/
|
*/
|
||||||
public abstract void updateMetrics();
|
public abstract void updateMetrics();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called every day at midnight to update
|
||||||
|
* the leaderboards.
|
||||||
|
*/
|
||||||
|
public abstract void updateLeaderboards();
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
public enum Platforms {
|
public enum Platforms {
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package cc.fascinated.platform.impl;
|
package cc.fascinated.platform.impl;
|
||||||
|
|
||||||
|
import cc.fascinated.common.MathUtils;
|
||||||
import cc.fascinated.model.score.TotalScoresResponse;
|
import cc.fascinated.model.score.TotalScoresResponse;
|
||||||
|
import cc.fascinated.model.score.TrackedScore;
|
||||||
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
||||||
|
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
|
import cc.fascinated.platform.CurvePoint;
|
||||||
import cc.fascinated.platform.Platform;
|
import cc.fascinated.platform.Platform;
|
||||||
import cc.fascinated.services.QuestDBService;
|
import cc.fascinated.services.QuestDBService;
|
||||||
import cc.fascinated.services.ScoreSaberService;
|
import cc.fascinated.services.ScoreSaberService;
|
||||||
@ -10,15 +14,35 @@ import cc.fascinated.services.TrackedScoreService;
|
|||||||
import cc.fascinated.services.UserService;
|
import cc.fascinated.services.UserService;
|
||||||
import io.questdb.client.Sender;
|
import io.questdb.client.Sender;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
|
@Log4j2
|
||||||
public class ScoreSaberPlatform extends Platform {
|
public class ScoreSaberPlatform extends Platform {
|
||||||
|
/**
|
||||||
|
* Delay in ms for requests per minute.
|
||||||
|
*/
|
||||||
|
private static final long UPDATE_DELAY = 1000L / 250L; // 250 requests per minute
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base multiplier for stars.
|
||||||
|
*/
|
||||||
|
private final double starMultiplier = 42.11;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The weight coefficient for the platform.
|
||||||
|
*/
|
||||||
|
private final double weightCoefficient = 0.965;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ScoreSaber service to use
|
* The ScoreSaber service to use
|
||||||
*/
|
*/
|
||||||
@ -46,13 +70,80 @@ public class ScoreSaberPlatform extends Platform {
|
|||||||
@Autowired
|
@Autowired
|
||||||
public ScoreSaberPlatform(@NonNull ScoreSaberService scoreSaberService, @NonNull UserService userService, @NonNull QuestDBService questDBService,
|
public ScoreSaberPlatform(@NonNull ScoreSaberService scoreSaberService, @NonNull UserService userService, @NonNull QuestDBService questDBService,
|
||||||
@NonNull TrackedScoreService trackedScoreService) {
|
@NonNull TrackedScoreService trackedScoreService) {
|
||||||
super(Platforms.SCORESABER);
|
super(Platforms.SCORESABER, 1, Map.of(
|
||||||
|
1, new CurvePoint[]{
|
||||||
|
new CurvePoint(1.0, 5.367394282890631),
|
||||||
|
new CurvePoint(0.9995, 5.019543595874787),
|
||||||
|
new CurvePoint(0.999, 4.715470646416203),
|
||||||
|
new CurvePoint(0.99825, 4.325027383589547),
|
||||||
|
new CurvePoint(0.9975, 3.996793606763322),
|
||||||
|
new CurvePoint(0.99625, 3.5526145337555373),
|
||||||
|
new CurvePoint(0.995, 3.2022017597337955),
|
||||||
|
new CurvePoint(0.99375, 2.9190155639254955),
|
||||||
|
new CurvePoint(0.99125, 2.4902905794106913),
|
||||||
|
new CurvePoint(0.99, 2.324506282149922),
|
||||||
|
new CurvePoint(0.9875, 2.058947159052738),
|
||||||
|
new CurvePoint(0.985, 1.8563887693647105),
|
||||||
|
new CurvePoint(0.9825, 1.697536248647543),
|
||||||
|
new CurvePoint(0.98, 1.5702410055532239),
|
||||||
|
new CurvePoint(0.9775, 1.4664726399289512),
|
||||||
|
new CurvePoint(0.975, 1.3807102743105126),
|
||||||
|
new CurvePoint(0.9725, 1.3090333065057616),
|
||||||
|
new CurvePoint(0.97, 1.2485807759957321),
|
||||||
|
new CurvePoint(0.965, 1.1552120359501035),
|
||||||
|
new CurvePoint(0.96, 1.0871883573850478),
|
||||||
|
new CurvePoint(0.955, 1.0388633331418984),
|
||||||
|
new CurvePoint(0.95, 1.0),
|
||||||
|
new CurvePoint(0.94, 0.9417362980580238),
|
||||||
|
new CurvePoint(0.93, 0.9039994071865736),
|
||||||
|
new CurvePoint(0.92, 0.8728710341448851),
|
||||||
|
new CurvePoint(0.91, 0.8488375988124467),
|
||||||
|
new CurvePoint(0.9, 0.825756123560842),
|
||||||
|
new CurvePoint(0.875, 0.7816934560296046),
|
||||||
|
new CurvePoint(0.85, 0.7462290664143185),
|
||||||
|
new CurvePoint(0.825, 0.7150465663454271),
|
||||||
|
new CurvePoint(0.8, 0.6872268862950283),
|
||||||
|
new CurvePoint(0.75, 0.6451808210101443),
|
||||||
|
new CurvePoint(0.7, 0.6125565959114954),
|
||||||
|
new CurvePoint(0.65, 0.5866010012767576),
|
||||||
|
new CurvePoint(0.6, 0.18223233667439062),
|
||||||
|
new CurvePoint(0.0, 0.0)
|
||||||
|
}
|
||||||
|
));
|
||||||
this.scoreSaberService = scoreSaberService;
|
this.scoreSaberService = scoreSaberService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.questDBService = questDBService;
|
this.questDBService = questDBService;
|
||||||
this.trackedScoreService = trackedScoreService;
|
this.trackedScoreService = trackedScoreService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the modifier for the given accuracy.
|
||||||
|
*
|
||||||
|
* @param accuracy The accuracy.
|
||||||
|
* @return The modifier.
|
||||||
|
*/
|
||||||
|
public double getModifier(int curveVersion, double accuracy) {
|
||||||
|
accuracy = MathUtils.clamp(accuracy, 0, 100) / 100;
|
||||||
|
|
||||||
|
CurvePoint prev = this.getCurvePoints().get(curveVersion)[0];
|
||||||
|
for (CurvePoint point : this.getCurvePoints().get(curveVersion)) {
|
||||||
|
if (point.getX() >= accuracy) {
|
||||||
|
double distance = (prev.getX() - accuracy) / (prev.getX() - point.getX());
|
||||||
|
return MathUtils.lerp(prev.getY(), point.getY(), distance);
|
||||||
|
}
|
||||||
|
prev = point;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getPp(int curveVersion, double stars, double accuracy) {
|
||||||
|
this.checkCurveVersion(curveVersion); // Check if the curve version exists
|
||||||
|
double pp = stars * this.starMultiplier;
|
||||||
|
double modifier = this.getModifier(curveVersion, accuracy);
|
||||||
|
return modifier * pp;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updatePlayers() {
|
public void updatePlayers() {
|
||||||
for (User user : this.userService.getUsers()) {
|
for (User user : this.userService.getUsers()) {
|
||||||
@ -89,4 +180,56 @@ public class ScoreSaberPlatform extends Platform {
|
|||||||
.atNow();
|
.atNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateLeaderboards() {
|
||||||
|
List<TrackedScore> scores = this.trackedScoreService.getTrackedScores(this.getPlatform(), true);
|
||||||
|
List<String> leaderboardIds = scores.stream().map(TrackedScore::getLeaderboardId).toList();
|
||||||
|
|
||||||
|
log.info("Updating {} leaderboards for platform '{}'",
|
||||||
|
leaderboardIds.size(),
|
||||||
|
this.getPlatform().getPlatformName()
|
||||||
|
);
|
||||||
|
|
||||||
|
int finished = 0;
|
||||||
|
|
||||||
|
// Update the leaderboards
|
||||||
|
for (String id : leaderboardIds) {
|
||||||
|
try {
|
||||||
|
ScoreSaberLeaderboardToken leaderboard = this.scoreSaberService.getLeaderboard(id, true); // Update the cached leaderboard
|
||||||
|
if (leaderboard == null) {
|
||||||
|
log.warn("Failed to update leaderboard '{}' for platform '{}'", id, this.getPlatform().getPlatformName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TrackedScore> toUpdate = scores.stream().filter(score -> score.getLeaderboardId().equals(id)).toList();
|
||||||
|
for (TrackedScore score : toUpdate) { // Update the scores
|
||||||
|
double pp = this.getPp(this.getCurrentCurveVersion(), leaderboard.getStars(), score.getAccuracy());
|
||||||
|
score.setPp(pp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toUpdate.isEmpty()) { // Save the scores
|
||||||
|
this.trackedScoreService.updateScores(toUpdate.toArray(TrackedScore[]::new));
|
||||||
|
}
|
||||||
|
finished++;
|
||||||
|
|
||||||
|
log.info("Updated leaderboard '{}' for platform '{}', changed {} scores. ({}/{})",
|
||||||
|
id,
|
||||||
|
this.getPlatform().getPlatformName(),
|
||||||
|
toUpdate.size(),
|
||||||
|
finished,
|
||||||
|
leaderboardIds.size()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sleep to prevent rate limiting
|
||||||
|
try {
|
||||||
|
Thread.sleep(UPDATE_DELAY);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("An error occurred while updating leaderboard '{}'", id, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package cc.fascinated.repository.couchdb;
|
package cc.fascinated.repository.couchdb;
|
||||||
|
|
||||||
import cc.fascinated.model.score.TrackedScore;
|
import cc.fascinated.model.score.TrackedScore;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.CrudRepository;
|
import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
@ -11,6 +13,16 @@ import java.util.List;
|
|||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
public interface TrackedScoreRepository extends CrudRepository<TrackedScore, String> {
|
public interface TrackedScoreRepository extends CrudRepository<TrackedScore, String> {
|
||||||
|
/**
|
||||||
|
* Updates the pp of a score.
|
||||||
|
*
|
||||||
|
* @param scoreId the ID of the score
|
||||||
|
* @param pp the new pp of the score
|
||||||
|
*/
|
||||||
|
@Modifying @Transactional
|
||||||
|
@Query(value = "UPDATE score SET pp = :pp WHERE score_id = :scoreId", nativeQuery = true)
|
||||||
|
void updateScorePp(@Param("scoreId") String scoreId, @Param("pp") double pp);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of top tracked scores
|
* Gets a list of top tracked scores
|
||||||
* sorted by pp from the platform
|
* sorted by pp from the platform
|
||||||
@ -22,6 +34,24 @@ public interface TrackedScoreRepository extends CrudRepository<TrackedScore, Str
|
|||||||
@Query(value = "SELECT * FROM score WHERE platform = :platform AND pp > 0 ORDER BY pp DESC LIMIT :amount", nativeQuery = true)
|
@Query(value = "SELECT * FROM score WHERE platform = :platform AND pp > 0 ORDER BY pp DESC LIMIT :amount", nativeQuery = true)
|
||||||
List<TrackedScore> findTopRankedScores(@Param("platform") String platform, @Param("amount") int amount);
|
List<TrackedScore> findTopRankedScores(@Param("platform") String platform, @Param("amount") int amount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all tracked scores from a platform.
|
||||||
|
*
|
||||||
|
* @param platform the platform to get the scores from
|
||||||
|
* @return the scores
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT * FROM score WHERE platform = :platform", nativeQuery = true)
|
||||||
|
List<TrackedScore> findAllByPlatform(String platform);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all tracked scores from a platform.
|
||||||
|
*
|
||||||
|
* @param platform the platform to get the scores from
|
||||||
|
* @return the scores
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT * FROM score WHERE platform = :platform AND pp > 0", nativeQuery = true)
|
||||||
|
List<TrackedScore> findAllByPlatformRankedOnly(String platform);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the total amount of scores
|
* Gets the total amount of scores
|
||||||
* for a platform.
|
* for a platform.
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
package cc.fascinated.repository.mongo;
|
||||||
|
|
||||||
|
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fascinated (fascinated7)
|
||||||
|
*/
|
||||||
|
public interface ScoreSaberLeaderboardRepository extends MongoRepository<ScoreSaberLeaderboardToken, String> {
|
||||||
|
}
|
31
src/main/java/cc/fascinated/services/MongoService.java
Normal file
31
src/main/java/cc/fascinated/services/MongoService.java
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package cc.fascinated.services;
|
||||||
|
|
||||||
|
import com.mongodb.client.MongoCollection;
|
||||||
|
import org.bson.Document;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fascinated (fascinated7)
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class MongoService {
|
||||||
|
public static MongoService INSTANCE;
|
||||||
|
private final MongoTemplate mongoTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MongoService(MongoTemplate mongo) {
|
||||||
|
INSTANCE = this;
|
||||||
|
this.mongoTemplate = mongo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the platforms collection
|
||||||
|
*
|
||||||
|
* @return The platforms collection
|
||||||
|
*/
|
||||||
|
public MongoCollection<Document> getPlatformsCollection() {
|
||||||
|
return mongoTemplate.getCollection("platforms");
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,13 @@
|
|||||||
package cc.fascinated.services;
|
package cc.fascinated.services;
|
||||||
|
|
||||||
|
import cc.fascinated.model.score.TrackedScore;
|
||||||
import cc.fascinated.platform.Platform;
|
import cc.fascinated.platform.Platform;
|
||||||
import cc.fascinated.platform.impl.ScoreSaberPlatform;
|
import cc.fascinated.platform.impl.ScoreSaberPlatform;
|
||||||
|
import cc.fascinated.repository.couchdb.TrackedScoreRepository;
|
||||||
|
import com.mongodb.client.model.Filters;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.bson.Document;
|
||||||
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.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
@ -11,6 +15,8 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
@ -18,24 +24,74 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
@Log4j2(topic = "PlatformService")
|
@Log4j2(topic = "PlatformService")
|
||||||
public class PlatformService {
|
public class PlatformService {
|
||||||
|
private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The loaded platforms.
|
* The loaded platforms.
|
||||||
*/
|
*/
|
||||||
private final List<Platform> platforms = new ArrayList<>();
|
private final List<Platform> platforms = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tracked score repository to use.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final TrackedScoreRepository trackedScoreRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PlatformService(@NonNull ApplicationContext context) {
|
public PlatformService(@NonNull ApplicationContext context, @NonNull TrackedScoreRepository trackedScoreRepository) {
|
||||||
|
this.trackedScoreRepository = trackedScoreRepository;
|
||||||
|
|
||||||
|
log.info("Registering platforms...");
|
||||||
registerPlatform(context.getBean(ScoreSaberPlatform.class));
|
registerPlatform(context.getBean(ScoreSaberPlatform.class));
|
||||||
|
log.info("Loaded %s platforms.".formatted(this.platforms.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 */1 * * * *")
|
/**
|
||||||
public void updatePlatforms() {
|
* Updates the platform metrics.
|
||||||
log.info("Updating %s platforms...".formatted(this.platforms.size()));
|
* <p>
|
||||||
|
* This method is scheduled to run every minute.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 */5 * * * *")
|
||||||
|
public void updateMetrics() {
|
||||||
|
log.info("Updating %s platform metrics...".formatted(this.platforms.size()));
|
||||||
for (Platform platform : this.platforms) {
|
for (Platform platform : this.platforms) {
|
||||||
platform.updatePlayers();
|
|
||||||
platform.updateMetrics();
|
platform.updateMetrics();
|
||||||
}
|
}
|
||||||
log.info("Finished updating platforms.");
|
log.info("Finished updating platform metrics.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the platform players.
|
||||||
|
* <p>
|
||||||
|
* This method is scheduled to run every 15 minutes.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 */15 * * * *")
|
||||||
|
public void updateScores() {
|
||||||
|
log.info("Updating %s platform players...".formatted(this.platforms.size()));
|
||||||
|
for (Platform platform : this.platforms) {
|
||||||
|
platform.updatePlayers();
|
||||||
|
}
|
||||||
|
log.info("Finished updating platform players.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the platform leaderboards.
|
||||||
|
* <p>
|
||||||
|
* This method is scheduled to run every day at midnight.
|
||||||
|
* This is to ensure that the leaderboards are up-to-date.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 0 0 * * *")
|
||||||
|
public void refreshPlatformLeaderboards() {
|
||||||
|
log.info("Refreshing platform leaderboards...");
|
||||||
|
EXECUTOR_SERVICE.execute(() -> {
|
||||||
|
for (Platform platform : this.platforms) {
|
||||||
|
platform.updateLeaderboards();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.info("Finished refreshing platform leaderboards.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,6 +101,32 @@ public class PlatformService {
|
|||||||
*/
|
*/
|
||||||
public void registerPlatform(Platform platform) {
|
public void registerPlatform(Platform platform) {
|
||||||
this.platforms.add(platform);
|
this.platforms.add(platform);
|
||||||
|
log.info(" - Registered platform '%s'".formatted(platform.getPlatform().getPlatformName()));
|
||||||
|
|
||||||
|
// Find the platform in the database
|
||||||
|
Document document = MongoService.INSTANCE.getPlatformsCollection().find(Filters.eq("_id", platform.getPlatform().getPlatformName())).first();
|
||||||
|
if (document == null) { // The platform was not found
|
||||||
|
document = new Document("_id", platform.getPlatform().getPlatformName());
|
||||||
|
MongoService.INSTANCE.getPlatformsCollection().insertOne(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the previous curve version
|
||||||
|
platform.setPreviousCurveVersion(document.getInteger("currentCurveVersion", platform.getCurrentCurveVersion()));
|
||||||
|
|
||||||
|
// The curve was updated
|
||||||
|
if (platform.getPreviousCurveVersion() != platform.getCurrentCurveVersion()) {
|
||||||
|
log.info(" - Updated previous curve version for platform '%s' to '%s'".formatted(
|
||||||
|
platform.getPlatform().getPlatformName(),
|
||||||
|
platform.getPreviousCurveVersion()
|
||||||
|
));
|
||||||
|
|
||||||
|
log.info("Updating scores for platform '%s'...".formatted(platform.getPlatform().getPlatformName()));
|
||||||
|
EXECUTOR_SERVICE.execute(platform::updateLeaderboards); // Update the leaderboards
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the document
|
||||||
|
document.put("currentCurveVersion", platform.getCurrentCurveVersion());
|
||||||
|
MongoService.INSTANCE.getPlatformsCollection().replaceOne(Filters.eq("_id", platform.getPlatform().getPlatformName()), document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,17 @@ package cc.fascinated.services;
|
|||||||
|
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
import cc.fascinated.exception.impl.BadRequestException;
|
||||||
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
||||||
|
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
|
import cc.fascinated.repository.mongo.ScoreSaberLeaderboardRepository;
|
||||||
import kong.unirest.core.HttpResponse;
|
import kong.unirest.core.HttpResponse;
|
||||||
import kong.unirest.core.Unirest;
|
import kong.unirest.core.Unirest;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@ -14,9 +20,21 @@ import org.springframework.stereotype.Service;
|
|||||||
public class ScoreSaberService {
|
public class ScoreSaberService {
|
||||||
private static final String SCORESABER_API = "https://scoresaber.com/api/";
|
private static final String SCORESABER_API = "https://scoresaber.com/api/";
|
||||||
private static final String GET_PLAYER_ENDPOINT = SCORESABER_API + "player/%s/full";
|
private static final String GET_PLAYER_ENDPOINT = SCORESABER_API + "player/%s/full";
|
||||||
|
private static final String GET_LEADERBOARD_ENDPOINT = SCORESABER_API + "leaderboard/by-id/%s/info";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the ScoreSaber account for a user.
|
* The ScoreSaber leaderboard repository to use.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final ScoreSaberLeaderboardRepository leaderboardRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ScoreSaberService(@NonNull ScoreSaberLeaderboardRepository leaderboardRepository) {
|
||||||
|
this.leaderboardRepository = leaderboardRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the account for a user.
|
||||||
*
|
*
|
||||||
* @param user the user to get the account for
|
* @param user the user to get the account for
|
||||||
* @return the ScoreSaber account
|
* @return the ScoreSaber account
|
||||||
@ -29,13 +47,49 @@ public class ScoreSaberService {
|
|||||||
|
|
||||||
HttpResponse<ScoreSaberAccountToken> response = Unirest.get(GET_PLAYER_ENDPOINT.formatted(user.getSteamId()))
|
HttpResponse<ScoreSaberAccountToken> response = Unirest.get(GET_PLAYER_ENDPOINT.formatted(user.getSteamId()))
|
||||||
.asObject(ScoreSaberAccountToken.class);
|
.asObject(ScoreSaberAccountToken.class);
|
||||||
|
|
||||||
if (response.getParsingError().isPresent()) { // Failed to parse the response
|
if (response.getParsingError().isPresent()) { // Failed to parse the response
|
||||||
throw new BadRequestException("Failed to parse ScoreSaber account for %s".formatted(user.getUsername()));
|
throw new BadRequestException("Failed to parse ScoreSaber account for '%s'".formatted(user.getUsername()));
|
||||||
}
|
}
|
||||||
if (response.getStatus() != 200) { // The response was not successful
|
if (response.getStatus() != 200) { // The response was not successful
|
||||||
throw new BadRequestException("Failed to get ScoreSaber account for %s".formatted(user.getUsername()));
|
throw new BadRequestException("Failed to get ScoreSaber account for '%s'".formatted(user.getUsername()));
|
||||||
}
|
}
|
||||||
return response.getBody();
|
return response.getBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a leaderboard for a leaderboard id.
|
||||||
|
*
|
||||||
|
* @param leaderboardId the leaderboard id to get the leaderboard for
|
||||||
|
* @return the ScoreSaber leaderboard
|
||||||
|
* @throws BadRequestException if an error occurred while getting the leaderboard
|
||||||
|
*/
|
||||||
|
public ScoreSaberLeaderboardToken getLeaderboard(String leaderboardId, boolean bypassCache) {
|
||||||
|
Optional<ScoreSaberLeaderboardToken> leaderboardOptional = leaderboardRepository.findById(leaderboardId);
|
||||||
|
if (leaderboardOptional.isPresent() && !bypassCache) { // The leaderboard is cached
|
||||||
|
return leaderboardOptional.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse<ScoreSaberLeaderboardToken> response = Unirest.get(GET_LEADERBOARD_ENDPOINT.formatted(leaderboardId))
|
||||||
|
.asObject(ScoreSaberLeaderboardToken.class);
|
||||||
|
if (response.getParsingError().isPresent()) { // Failed to parse the response
|
||||||
|
throw new BadRequestException("Failed to parse ScoreSaber leaderboard for '%s'".formatted(leaderboardId));
|
||||||
|
}
|
||||||
|
if (response.getStatus() != 200) { // The response was not successful
|
||||||
|
throw new BadRequestException("Failed to get ScoreSaber leaderboard for '%s'".formatted(leaderboardId));
|
||||||
|
}
|
||||||
|
ScoreSaberLeaderboardToken leaderboard = response.getBody();
|
||||||
|
leaderboardRepository.save(leaderboard);
|
||||||
|
return leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a leaderboard for a leaderboard id.
|
||||||
|
*
|
||||||
|
* @param leaderboardId the leaderboard id to get the leaderboard for
|
||||||
|
* @return the ScoreSaber leaderboard
|
||||||
|
* @throws BadRequestException if an error occurred while getting the leaderboard
|
||||||
|
*/
|
||||||
|
public ScoreSaberLeaderboardToken getLeaderboard(String leaderboardId) {
|
||||||
|
return getLeaderboard(leaderboardId, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,8 @@ public class TrackedScoreService {
|
|||||||
/**
|
/**
|
||||||
* The tracked score repository to use.
|
* The tracked score repository to use.
|
||||||
*/
|
*/
|
||||||
@NonNull private final TrackedScoreRepository trackedScoreRepository;
|
@NonNull
|
||||||
|
private final TrackedScoreRepository trackedScoreRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository) {
|
public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository) {
|
||||||
@ -77,4 +78,29 @@ public class TrackedScoreService {
|
|||||||
trackedScoreRepository.countTotalRankedScores(platform.getPlatformName())
|
trackedScoreRepository.countTotalRankedScores(platform.getPlatformName())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of tracked scores
|
||||||
|
* for a platform.
|
||||||
|
*
|
||||||
|
* @param platform the platform to get the scores from
|
||||||
|
* @return the tracked scores
|
||||||
|
*/
|
||||||
|
public List<TrackedScore> getTrackedScores(Platform.Platforms platform, boolean ranked) {
|
||||||
|
if (ranked) {
|
||||||
|
return trackedScoreRepository.findAllByPlatformRankedOnly(platform.getPlatformName());
|
||||||
|
}
|
||||||
|
return trackedScoreRepository.findAllByPlatform(platform.getPlatformName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a list of tracked scores.
|
||||||
|
*
|
||||||
|
* @param scores the scores to save
|
||||||
|
*/
|
||||||
|
public void updateScores(TrackedScore... scores) {
|
||||||
|
for (TrackedScore score : scores) {
|
||||||
|
this.trackedScoreRepository.updateScorePp(score.getScoreId(), score.getPp());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user