Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
20d5aaa
Refactor image caching logic in CacheService.java
ZayrexDev Aug 24, 2026
ef7c1b0
Update color codes and adjust layout in multiplayer-room-result.html
ZayrexDev Aug 24, 2026
9285903
Update color codes and adjust layout in multiplayer-room-result.html
ZayrexDev Aug 24, 2026
0b92124
Enhance score analysis with lazer score detection and simulation matc…
ZayrexDev Aug 28, 2026
c570bef
Bump version to 1.11.2
ZayrexDev Aug 28, 2026
bda794b
Optimized random score selection to weight scores according to their …
ZayrexDev Sep 1, 2026
49e82b7
Add support for @user showcase targets
ZayrexDev Sep 1, 2026
4307ebc
Adjusted weight and make danser obscure more info
ZayrexDev Sep 1, 2026
634fd76
Adjusted weight
ZayrexDev Sep 1, 2026
9917a5f
Adjusted weight
ZayrexDev Sep 1, 2026
79c313e
Add function to get random score from given user list
ZayrexDev Sep 1, 2026
b001312
Updated README.md
ZayrexDev Sep 1, 2026
7ac442a
Fix key name in randomScoreFromUsers method to use 'uids' instead of …
ZayrexDev Sep 1, 2026
ab503cd
Adjust pattern weights and rename score mapping method
ZayrexDev Sep 1, 2026
bc69254
Shuffle user IDs before finding available score
ZayrexDev Sep 1, 2026
184a3a7
Refactor score replay checks and update HTML condition for replay dis…
ZayrexDev Sep 1, 2026
8c8a90f
Hide Mods section in danser-config-patch-obscured.json
ZayrexDev Sep 1, 2026
e42fedc
Make score-analysis.html use cv. UR
ZayrexDev Sep 1, 2026
9b5936c
Add user rank endpoint and refactor score filtering methods
ZayrexDev Sep 1, 2026
23565d2
Adjusted weight
ZayrexDev Sep 1, 2026
12619a6
Adjusted weight
ZayrexDev Sep 1, 2026
4f9aab0
Minor fixes in ScoreController
ZayrexDev Sep 1, 2026
6b67a85
Minor fixes in ScoreController
ZayrexDev Sep 1, 2026
8e8486d
Minor fixes in ScoreController
ZayrexDev Sep 1, 2026
4edec0e
Enhance score display with replay icon and style adjustments
ZayrexDev Sep 2, 2026
f791850
Enhance score display with replay icon and style adjustments
ZayrexDev Sep 2, 2026
961bb99
Add replay filter support in ScoreFilter
ZayrexDev Sep 2, 2026
184b32e
Update version to 1.11.5 and refactor ScoreController to use Weighted…
ZayrexDev Sep 2, 2026
f1dcb3c
Validate weights in ScoreController and WeightedRandom to ensure they…
ZayrexDev Sep 2, 2026
e6e8c1a
Add endpoint to retrieve weighted scores for a user and refactor scor…
ZayrexDev Sep 3, 2026
364aa3b
Add endpoint to retrieve weighted scores for a user and refactor scor…
ZayrexDev Sep 3, 2026
e65e5ce
Refactor weight calculation in ScoreController to use BeatmapAnalyzer…
ZayrexDev Sep 3, 2026
4086b1c
Enhance random score calculation in ScoreController to incorporate we…
ZayrexDev Sep 4, 2026
824c936
Improved weight calculation
ZayrexDev Sep 4, 2026
66d9ad3
Improved weight calculation
ZayrexDev Sep 4, 2026
0a98f1a
Expand return cap
ZayrexDev Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 25 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ Image endpoints return PNG bytes. Replay download returns `video/mp4`.

### Beatmaps

| Method | Path | Purpose | Params / POST Body | Response |
|--------|--------------------------------------|--------------------------------|----------------------------------------------------|----------|
| GET | `/beatmaps/lookup` | Resolve beatmap IDs | See section below | JSON |
| GET | `/beatmaps/{beatmapId}` | Beatmap card image | path `beatmapId` (+ optional query param `mod`) | PNG |
| GET | `/beatmaps/{beatmapId}/analysis` | PP+ and object-pattern analysis | path `beatmapId` (+ optional query param `mod`) | PNG |
| POST | `/beatmaps/{beatmapId}/leaderboards` | Compare players on one beatmap | path `beatmapId` + POST Body `{"uids":[user ids]}` | PNG |
| Method | Path | Purpose | Params / POST Body | Response |
|--------|--------------------------------------|---------------------------------|----------------------------------------------------|----------|
| GET | `/beatmaps/lookup` | Resolve beatmap IDs | See section below | JSON |
| GET | `/beatmaps/{beatmapId}` | Beatmap card image | path `beatmapId` (+ optional query param `mod`) | PNG |
| GET | `/beatmaps/{beatmapId}/analysis` | PP+ and object-pattern analysis | path `beatmapId` (+ optional query param `mod`) | PNG |
| POST | `/beatmaps/{beatmapId}/leaderboards` | Compare players on one beatmap | path `beatmapId` + POST Body `{"uids":[user ids]}` | PNG |

### Beatmapsets

Expand All @@ -123,14 +123,16 @@ Image endpoints return PNG bytes. Replay download returns `video/mp4`.

### Scores

| Method | Path | Purpose | Params / POST Body | Response |
|--------|--------------------------------------------------|--------------------------------|----------------------------|----------|
| GET | `/scores/lookup` | Resolve score IDs | See section below | JSON |
| GET | `/scores/{scoreId}` | Score card image | path `scoreId` | PNG |
| GET | `/scores/{scoreId}/analysis` | Score analysis card image | path `scoreId` | PNG |
| GET | `/scores/{scoreId}/highlight` | 20s highlight range of a score | path `scoreId` | JSON |
| GET | `/scores/{scoreId}/misses` | List the misses of the score | path `scoreId` | JSON |
| GET | `/scores/{scoreId}/misses/{missIndex}/visualize` | Visualize misses | path `scoreId` `missIndex` | PNG |
| Method | Path | Purpose | Params / POST Body | Response |
|--------|--------------------------------------------------|-------------------------------------|---------------------------------|----------|
| GET | `/scores/lookup` | Resolve score IDs | See section below | JSON |
| GET | `/scores/random` | Get a random score | `min_rank` | JSON |
| POST | `/scores/random/users` | Get a random score from given users | POST Body `{"uids":[user ids]}` | JSON |
| GET | `/scores/{scoreId}` | Score card image | path `scoreId` | PNG |
| GET | `/scores/{scoreId}/analysis` | Score analysis card image | path `scoreId` | PNG |
| GET | `/scores/{scoreId}/highlight` | 20s highlight range of a score | path `scoreId` | JSON |
| GET | `/scores/{scoreId}/misses` | List the misses of the score | path `scoreId` | JSON |
| GET | `/scores/{scoreId}/misses/{missIndex}/visualize` | Visualize misses | path `scoreId` `missIndex` | PNG |

### Multiplayer Rooms

Expand All @@ -141,15 +143,15 @@ Image endpoints return PNG bytes. Replay download returns `video/mp4`.

### Users

| Method | Path | Purpose | Params / POST Body | Response |
|--------|---------------------------------|---------------------------|----------------------------------|----------|
| POST | `/users` | Get multiple user data | POST Body `{"ids":[user ids]}` | JSON |
| GET | `/users/me` | User data | Requires Authorization Header | JSON |
| GET | `/users/me/friends` | Friends list for user | Requires Authorization Header | JSON |
| POST | `/users/leaderboards` | User PP leaderboard image | `{"uids":[user ids]}` | PNG |
| GET | `/users/{userId}/scores/bestof` | Best-of-N scores image | path `userId`, query `n` (count) | PNG |
| GET | `/users/{userId}/scores/recent` | Recent scores image | path `userId`, query `n` (count) | PNG |
| GET | `/users/{userId}/scores/today-best` | Best scores achieved recently | path `userId`, optional query `days` (default 1) | PNG |
| Method | Path | Purpose | Params / POST Body | Response |
|--------|-------------------------------------|-------------------------------|--------------------------------------------------|----------|
| POST | `/users` | Get multiple user data | POST Body `{"ids":[user ids]}` | JSON |
| GET | `/users/me` | User data | Requires Authorization Header | JSON |
| GET | `/users/me/friends` | Friends list for user | Requires Authorization Header | JSON |
| POST | `/users/leaderboards` | User PP leaderboard image | `{"uids":[user ids]}` | PNG |
| GET | `/users/{userId}/scores/bestof` | Best-of-N scores image | path `userId`, query `n` (count) | PNG |
| GET | `/users/{userId}/scores/recent` | Recent scores image | path `userId`, query `n` (count) | PNG |
| GET | `/users/{userId}/scores/today-best` | Best scores achieved recently | path `userId`, optional query `days` (default 1) | PNG |

### Replays (enabled when `replayRender.enabled` is true)

Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.github.BotSeira</groupId>
<artifactId>oStella</artifactId>
<version>1.11.1</version>
<version>1.11.5</version>

<properties>
<maven.compiler.source>25</maven.compiler.source>
Expand Down Expand Up @@ -118,7 +118,7 @@
<dependency>
<groupId>com.github.BotSeira</groupId>
<artifactId>osuParser</artifactId>
<version>v1.5.1</version>
<version>v1.5.2</version>
</dependency>

<!-- Source: https://mvnrepository.com/artifact/org.tukaani/xz -->
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/xyz/zcraft/ostella/data/ScoreFilter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package xyz.zcraft.ostella.data;

import xyz.zcraft.ostella.service.CacheService;
import xyz.zcraft.osu.model.Mod;
import xyz.zcraft.osu.model.Score;

Expand All @@ -14,7 +15,7 @@
/** A validated filter applied to a score-list response. */
public final class ScoreFilter {
private static final Pattern FILTER_PATTERN = Pattern.compile(
"(?i)^(acc(?:uracy)?|combo|pp|time|length|len|star|stars|sr|bpm|miss|misses|score|mod|mods|rank"
"(?i)^(acc(?:uracy)?|combo|pp|time|length|len|star|stars|sr|bpm|miss|misses|score|mod|mods|rank|replay"
+ "|title|artist|mapper|genre|language|video|storyboard|fullcombo)"
+ "(>=|<=|!=|!~|>|<|=|~)(.+)$"
);
Expand Down Expand Up @@ -228,7 +229,7 @@ private static String formatValue(Field field, double value) {
case BPM -> formatNumber(value) + " BPM";
case MISS -> formatNumber(value) + " miss";
case SCORE -> formatNumber(value);
case MODS, RANK, TITLE, ARTIST, MAPPER, GENRE, LANGUAGE, VIDEO, STORYBOARD, FULL_COMBO ->
case MODS, RANK, TITLE, ARTIST, MAPPER, GENRE, LANGUAGE, VIDEO, STORYBOARD, FULL_COMBO, REPLAY ->
throw new IllegalStateException("Text filter has no numeric value");
};
}
Expand Down Expand Up @@ -274,6 +275,7 @@ public boolean matches(Score score) {
&& compareText(score.getBeatmapset().getLanguage().getName());
case VIDEO -> score.getBeatmapset() != null && compareBoolean(score.getBeatmapset().getVideo());
case STORYBOARD -> score.getBeatmapset() != null && compareBoolean(score.getBeatmapset().getStoryboard());
case REPLAY -> compareBoolean(score.getHasReplay() || CacheService.hasReplayCache(score.getId()));
case FULL_COMBO -> compareBoolean(score.getIsPerfectCombo());
};
}
Expand Down Expand Up @@ -343,7 +345,7 @@ private boolean compareText(String... actualValues) {
private boolean compareBoolean(Boolean actual) {
if (actual == null) return false;
boolean equal = actual == Boolean.parseBoolean(textValues.iterator().next());
return operator == Operator.EQUAL ? equal : !equal;
return (operator == Operator.EQUAL) == equal;
}

public String displayText() {
Expand All @@ -368,6 +370,7 @@ private enum Field {
LANGUAGE("Language"),
VIDEO("Video"),
STORYBOARD("Storyboard"),
REPLAY("Replay"),
FULL_COMBO("Full combo");

private final String label;
Expand Down Expand Up @@ -395,6 +398,7 @@ static Field from(String value) {
case "language" -> LANGUAGE;
case "video" -> VIDEO;
case "storyboard" -> STORYBOARD;
case "replay" -> REPLAY;
case "fullcombo" -> FULL_COMBO;
default -> throw new IllegalArgumentException("Unknown filter field: " + value);
};
Expand All @@ -405,7 +409,7 @@ boolean isMetadataText() {
}

boolean isBoolean() {
return this == VIDEO || this == STORYBOARD || this == FULL_COMBO;
return this == VIDEO || this == STORYBOARD || this == FULL_COMBO || this == REPLAY;
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/xyz/zcraft/ostella/network/WebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public WebServer(AppConfig conf, TokenManager tokenManager) throws IOException {

.get("/scores/lookup", router.scoreController::lookupScore)
.get("/scores/random", router.scoreController::randomScore)
.post("/scores/random/users", router.scoreController::randomScoreFromUsers)
.get("/scores/random/users/{userId}/weights", router.scoreController::randomScoreFromUsersWeights)
.post("/scores/random/users/{userId}/weights", router.scoreController::randomScoreFromUsersWeights)
.get("/scores/{scoreId}", router.scoreController::renderScoreById)
.get("/scores/{scoreId}/analysis", router.analyzeController::renderScoreAnalysisById)
.get("/scores/{scoreId}/highlight", router.analyzeController::getScoreHighlight)
Expand All @@ -77,6 +80,7 @@ public WebServer(AppConfig conf, TokenManager tokenManager) throws IOException {
.post("/users/leaderboards", router.leaderboardController::getLeaderboard)
.post("/users/scores/recent/batch", router.userController::getRecentScoresBatch)
.get("/users/{userId}", router.userController::getUserInfo)
.get("/users/{userId}/rank", router.userController::getUserRank)
.get("/users/{userId}/scores/bestof", router.userController::getBestOfN)
.get("/users/{userId}/scores/recent", router.userController::getRecentScores)
.get("/users/{userId}/scores/today-best", router.userController::getTodayBestScores)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import xyz.zcraft.ostella.service.MissVisualizeService;
import xyz.zcraft.ostella.service.RenderService;
import xyz.zcraft.ostella.util.TokenManager;
import xyz.zcraft.ostella.util.format.ScoreFormatUtil;
import xyz.zcraft.osu.model.BeatmapExtended;
import xyz.zcraft.osu.model.Mod;
import xyz.zcraft.osu.model.Score;
Expand All @@ -34,6 +35,7 @@

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

Expand Down Expand Up @@ -299,6 +301,31 @@ public void renderScoreAnalysisById(@NotNull Context context) {
throw new ApiException(ErrorCode.SCORE_PARSE_FAILED, e);
}

boolean isLazerScore =
osuReplay.gameVersion() > 30000001
&& score.getMods().stream()
.map(Mod::getAcronym)
.noneMatch(s -> Objects.equals(s, "CL") || Objects.equals(s, "V2"));

HashMap<HitEvent.HitResult, Integer> simResult = new HashMap<>();
analyze.events().stream()
.filter(e -> e.eventType().equals(HitEvent.EventType.HIT_CIRCLE)
|| e.eventType().equals(HitEvent.EventType.SLIDER_HEAD))
.forEach(e -> simResult.merge(e.hitResult(), 1, Integer::sum));

final Integer simGreat = simResult.getOrDefault(HitEvent.HitResult.PERFECT, 0);
final Integer simOk = simResult.getOrDefault(HitEvent.HitResult.OK, 0);
final Integer simMeh = simResult.getOrDefault(HitEvent.HitResult.MEH, 0);
final Integer simMiss = simResult.getOrDefault(HitEvent.HitResult.MISS, 0);

String simHitResult = simGreat + " / " + simOk + " / " + simMeh + " / " + simMiss;

boolean doSimMatch =
simGreat == ScoreFormatUtil.getGreatCount(score)
&& simOk == ScoreFormatUtil.getOkCount(score)
&& simMeh == ScoreFormatUtil.getMehCount(score)
&& simMiss == ScoreFormatUtil.getMissCount(score);

return perfPlusApi.calculate(score)
.exceptionally(error -> {
LOG.warn("Could not calculate performance+ for score {}. "
Expand All @@ -309,7 +336,7 @@ public void renderScoreAnalysisById(@NotNull Context context) {
.thenApply(performancePlus -> new ScoreAnalyzeData(
score, diffSpec, hitErrors, hitPos, hitPosAbs,
missPos, missPosAbs, aimBias, avgTimingError, analyze,
performanceGraph, performancePlus));
performanceGraph, performancePlus, isLazerScore, doSimMatch, simHitResult));
})
.thenApplyAsync(renderer::renderScoreAnalysis, renderer.getRenderExecutor())
.thenAccept(bytes -> context.status(200).result(bytes)));
Expand Down Expand Up @@ -406,7 +433,10 @@ public record ScoreAnalyzeData(
double avgTimingError,
ReplayAnalyze replayAnalyze,
PerformanceGraphData performanceGraph,
PerfPlusApi.PerformancePlus performancePlus
PerfPlusApi.PerformancePlus performancePlus,
boolean isLazerScore,
boolean doSimMatch,
String simHitResult
) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,11 @@ public void renderShowcaseOfUsers(@NotNull Context context) {
List<CompletableFuture<Score>> scoreFutures = new ArrayList<>(ids.stream()
.filter(id -> !id.startsWith("s"))
.map(id -> {
if (id.startsWith("u")) return id.substring(1);
if (id.startsWith("u")) {
return id.substring(1);
} else if (id.startsWith("@")) {
return String.valueOf(executor.enqueueAsync(() -> OsuAPI.getUser(tokenManager.getTokenData(), id.substring(1))).join().getId());
}
return id;
})
.map(Long::parseLong)
Expand Down
Loading
Loading