Added replay-viewer support for replay pairs
This commit is contained in:
parent
8ca3fa70b6
commit
e413b2d76e
@ -102,6 +102,15 @@ data class ReplayViewerData(
|
||||
val judgements: List<CircleguardService.ScoreJudgement>
|
||||
)
|
||||
|
||||
data class ReplayPairViewerData(
|
||||
val beatmap: String,
|
||||
val replay1: String,
|
||||
val replay2: String,
|
||||
val mods: Int,
|
||||
val judgements1: List<CircleguardService.ScoreJudgement>,
|
||||
val judgements2: List<CircleguardService.ScoreJudgement>
|
||||
)
|
||||
|
||||
data class ReplayData(
|
||||
val replay_id: Long,
|
||||
val user_id: Int,
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
package com.nisemoe.nise.controller
|
||||
|
||||
import com.nisemoe.nise.Format
|
||||
import com.nisemoe.nise.ReplayData
|
||||
import com.nisemoe.nise.ReplayPair
|
||||
import com.nisemoe.nise.ReplayViewerData
|
||||
import com.nisemoe.nise.*
|
||||
import com.nisemoe.nise.database.ScoreService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.CrossOrigin
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
@ -32,6 +28,14 @@ class ScoreController(
|
||||
return ResponseEntity.ok(replayData)
|
||||
}
|
||||
|
||||
@GetMapping("pair/{replay1Id}/{replay2Id}/replay")
|
||||
fun getPairReplays(@PathVariable replay1Id: Long, @PathVariable replay2Id: Long): ResponseEntity<ReplayPairViewerData> {
|
||||
val replayPairViewerData = this.scoreService.getReplayPairViewerData(replay1Id, replay2Id)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
|
||||
return ResponseEntity.ok(replayPairViewerData)
|
||||
}
|
||||
|
||||
@GetMapping("pair/{replay1Id}/{replay2Id}")
|
||||
fun getPairDetails(@PathVariable replay1Id: Long, @PathVariable replay2Id: Long): ResponseEntity<ReplayPair> {
|
||||
val replay1Data = this.scoreService.getReplayData(replay1Id)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.nisemoe.nise.database
|
||||
|
||||
import com.nisemoe.generated.enums.JudgementType
|
||||
import com.nisemoe.generated.tables.records.ScoresJudgementsRecord
|
||||
import com.nisemoe.generated.tables.records.ScoresRecord
|
||||
import com.nisemoe.generated.tables.references.*
|
||||
@ -48,6 +49,20 @@ class ScoreService(
|
||||
.mapNotNull { (scoreType, title) -> db.get(scoreType)?.let { ReplayDataChart(title, it.filterNotNull()) } }
|
||||
}
|
||||
|
||||
fun getReplayPairViewerData(replay1Id: Long, replay2Id: Long): ReplayPairViewerData? {
|
||||
val replay1 = getReplayViewerData(replay1Id) ?: return null
|
||||
val replay2 = getReplayViewerData(replay2Id) ?: return null
|
||||
|
||||
return ReplayPairViewerData(
|
||||
beatmap = replay1.beatmap,
|
||||
replay1 = replay1.replay,
|
||||
replay2 = replay2.replay,
|
||||
mods = replay1.mods,
|
||||
judgements1 = replay1.judgements,
|
||||
judgements2 = replay2.judgements
|
||||
)
|
||||
}
|
||||
|
||||
fun getReplayViewerData(replayId: Long): ReplayViewerData? {
|
||||
val beatmapId = dslContext.select(SCORES.BEATMAP_ID)
|
||||
.from(SCORES)
|
||||
@ -463,6 +478,15 @@ class ScoreService(
|
||||
replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
|
||||
}
|
||||
|
||||
fun mapLegacyJudgement(judgementType: JudgementType): CircleguardService.JudgementType {
|
||||
return when(judgementType) {
|
||||
JudgementType.Miss -> CircleguardService.JudgementType.MISS
|
||||
JudgementType.`300` -> CircleguardService.JudgementType.THREE_HUNDRED
|
||||
JudgementType.`100` -> CircleguardService.JudgementType.ONE_HUNDRED
|
||||
JudgementType.`50` -> CircleguardService.JudgementType.FIFTY
|
||||
}
|
||||
}
|
||||
|
||||
fun getJudgements(replayId: Long): List<CircleguardService.ScoreJudgement> {
|
||||
val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
|
||||
.from(SCORES)
|
||||
@ -486,7 +510,7 @@ class ScoreService(
|
||||
distance_center = it.distanceCenter!!,
|
||||
distance_edge = it.distanceEdge!!,
|
||||
time = it.time!!,
|
||||
type = CircleguardService.JudgementType.valueOf(it.type!!.literal),
|
||||
type = mapLegacyJudgement(it.type!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>/replay/</title>
|
||||
<title>/replay/ - nise.moe</title>
|
||||
<link rel="icon" type="image/x-icon" href="https://nise.moe/assets/favicon.ico">
|
||||
<!-- Embed data -->
|
||||
<meta property="og:title" content="/nise.moe/ - osu!cheaters finder">
|
||||
|
||||
BIN
nise-replay-viewer/public/cursor2.png
Normal file
BIN
nise-replay-viewer/public/cursor2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -2,28 +2,35 @@ import {AboutDialog} from "./composites/about-dialog";
|
||||
import {Navbar} from "./composites/Menu";
|
||||
import {SongSlider} from "./composites/song-slider";
|
||||
import {Helper} from "./composites/helper";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useEffect} from "react";
|
||||
import {OsuRenderer} from "@/osu/OsuRenderer";
|
||||
import {Stats} from "@/interface/composites/stats";
|
||||
|
||||
export function App() {
|
||||
|
||||
const [replayId, setReplayId] = useState<number>(0);
|
||||
const loadReplay = async (replayId: number) => {
|
||||
await OsuRenderer.loadReplayFromUrl(replayId);
|
||||
};
|
||||
|
||||
const loadReplayPair = async (replayId1: number, replayId2: number) => {
|
||||
await OsuRenderer.loadReplayPairFromUrl(replayId1, replayId2);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let pathReplayId = Number.parseInt(location.pathname.slice(1, location.pathname.length));
|
||||
// This pattern matches one or more digits followed by an optional slash and any characters (non-greedy)
|
||||
const pathRegex = /^\/(\d+)(?:\/(\d+))?/;
|
||||
const match = location.pathname.match(pathRegex);
|
||||
|
||||
const loadReplay = async () => {
|
||||
if(document.location.hostname === "localhost") {
|
||||
await OsuRenderer.loadReplayFromUrl(`http://localhost:8080/score/${pathReplayId}/replay`, pathReplayId);
|
||||
return;
|
||||
if (match) {
|
||||
// match[1] will contain the first ID, match[2] (if present) will contain the second ID
|
||||
const pathReplayId1 = Number.parseInt(match[1]);
|
||||
const pathReplayId2 = match[2] ? Number.parseInt(match[2]) : null;
|
||||
|
||||
if(pathReplayId2 != null) {
|
||||
loadReplayPair(pathReplayId1, pathReplayId2);
|
||||
} else {
|
||||
loadReplay(pathReplayId1);
|
||||
}
|
||||
await OsuRenderer.loadReplayFromUrl(`https://nise.moe/api/score/${pathReplayId}/replay`, pathReplayId);
|
||||
};
|
||||
|
||||
if(replayId !== pathReplayId) {
|
||||
setReplayId(pathReplayId);
|
||||
loadReplay();
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
|
||||
@ -23,11 +23,16 @@ export function Navbar() {
|
||||
|
||||
{OsuRenderer.beatmap && (
|
||||
<>
|
||||
{" "}
|
||||
{OsuRenderer.replay2 == null && (
|
||||
<a href={"https://nise.moe/s/" + OsuRenderer.replay.info.id} target="_blank"
|
||||
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
|
||||
View on nise.moe
|
||||
</a>
|
||||
</a>)}
|
||||
{OsuRenderer.replay2 && (
|
||||
<a href={"https://nise.moe/p/" + OsuRenderer.replay.info.id + "/" + OsuRenderer.replay2.info.id} target="_blank"
|
||||
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
|
||||
View on nise.moe
|
||||
</a>)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -37,7 +42,7 @@ export function Navbar() {
|
||||
{mods?.map((mod) => {
|
||||
return (
|
||||
<img
|
||||
src={`./mod_${mod.acronym.toLowerCase()}.png`}
|
||||
src={`/mod_${mod.acronym.toLowerCase()}.png`}
|
||||
className="h-5"
|
||||
></img>
|
||||
);
|
||||
|
||||
@ -10,6 +10,7 @@ export class Drawer {
|
||||
|
||||
static images = {
|
||||
cursor: undefined as any as p5.Image,
|
||||
cursor2: undefined as any as p5.Image,
|
||||
cursortrail: undefined as any as p5.Image,
|
||||
hitcircle: undefined as any as p5.Image,
|
||||
hitcircleoverlay: undefined as any as p5.Image,
|
||||
@ -183,6 +184,7 @@ export class Drawer {
|
||||
|
||||
|
||||
static drawCursorPath(
|
||||
cursorImage: p5.Image,
|
||||
path: {
|
||||
position: Vector2;
|
||||
time: number;
|
||||
@ -251,7 +253,7 @@ export class Drawer {
|
||||
|
||||
if (cursor.position)
|
||||
Drawer.p.image(
|
||||
this.images.cursor,
|
||||
cursorImage,
|
||||
cursor.position.x,
|
||||
cursor.position.y,
|
||||
55,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {HitResult, LegacyReplayFrame, Score, Vector2} from "osu-classes";
|
||||
import {HitResult, LegacyReplayFrame, Replay, Score, Vector2} from "osu-classes";
|
||||
import {BeatmapDecoder, BeatmapEncoder} from "osu-parsers";
|
||||
import {
|
||||
Circle,
|
||||
@ -15,6 +15,7 @@ import {Vec2} from "@osujs/math";
|
||||
import {clamp, getBeatmap, getReplay} from "@/utils";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import {toast} from "sonner";
|
||||
import p5 from "p5";
|
||||
|
||||
export enum OsuRendererEvents {
|
||||
UPDATE = "UPDATE",
|
||||
@ -56,6 +57,8 @@ export class OsuRenderer {
|
||||
static event = new OsuRendererBridge();
|
||||
|
||||
static judgements: Judgements[] = [];
|
||||
static judgements2: Judgements[] = [];
|
||||
|
||||
static speedMultiplier = 1;
|
||||
static settings: OsuRendererSettings = {
|
||||
showCursorPath: true,
|
||||
@ -73,7 +76,10 @@ export class OsuRenderer {
|
||||
|
||||
static beatmap: StandardBeatmap;
|
||||
static og_beatmap: StandardBeatmap;
|
||||
|
||||
static replay: Score;
|
||||
static replay2: Score | undefined;
|
||||
|
||||
static og_replay_mods: StandardModCombination;
|
||||
static forceHR: boolean | undefined = undefined;
|
||||
|
||||
@ -137,7 +143,11 @@ export class OsuRenderer {
|
||||
|
||||
this.lastRender = Date.now();
|
||||
|
||||
this.renderPath();
|
||||
this.renderPath(this.replay.replay!, Drawer.images.cursor);
|
||||
if(this.replay2) {
|
||||
this.renderPath(this.replay2.replay!, Drawer.images.cursor2);
|
||||
}
|
||||
|
||||
Drawer.drawField();
|
||||
}
|
||||
|
||||
@ -216,9 +226,56 @@ export class OsuRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
static async loadReplayFromUrl(url: string, replayId: number) {
|
||||
static getApiUrl(): string {
|
||||
return document.location.hostname === "localhost"
|
||||
? `http://localhost:8080`
|
||||
: `https://nise.moe/api`;
|
||||
}
|
||||
|
||||
static async loadReplayPairFromUrl(replayId1: number, replayId2: number) {
|
||||
OsuRenderer.purge();
|
||||
|
||||
const apiUrl = `${this.getApiUrl()}/pair/${replayId1}/${replayId2}/replay`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: {
|
||||
'X-NISE-REPLAY': '20240303'
|
||||
}
|
||||
});
|
||||
|
||||
let data;
|
||||
|
||||
if (!response.ok) {
|
||||
toast.error("Failed to load replay :(");
|
||||
return Promise.reject();
|
||||
} else {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
const { beatmap, replay1, replay2, mods, judgements1, judgements2 } = data;
|
||||
|
||||
// Load replays
|
||||
const i_replay1 = await getReplay(replay1);
|
||||
i_replay1.info.id = replayId1;
|
||||
i_replay1.info.rawMods = mods;
|
||||
i_replay1.info.mods = new StandardModCombination(mods);
|
||||
|
||||
const i_replay2 = await getReplay(replay2);
|
||||
i_replay2.info.id = replayId2;
|
||||
i_replay2.info.rawMods = mods;
|
||||
i_replay2.info.mods = new StandardModCombination(mods);
|
||||
|
||||
const i_beatmap = await getBeatmap(beatmap, i_replay1);
|
||||
|
||||
OsuRenderer.setPairOptions(i_beatmap, i_replay1, i_replay2, judgements1, judgements2);
|
||||
this.event.emit(OsuRendererEvents.LOAD);
|
||||
}
|
||||
|
||||
static async loadReplayFromUrl(replayId: number) {
|
||||
OsuRenderer.purge();
|
||||
|
||||
const url = `${this.getApiUrl()}/score/${replayId}/replay`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-NISE-REPLAY': '20240303'
|
||||
@ -247,6 +304,22 @@ export class OsuRenderer {
|
||||
this.event.emit(OsuRendererEvents.LOAD);
|
||||
}
|
||||
|
||||
static setPairOptions(beatmap: StandardBeatmap, replay1: Score, replay2: Score, judgements1: Judgements[], judgements2: Judgements[]) {
|
||||
this.judgements = judgements1;
|
||||
this.judgements2 = judgements2;
|
||||
this.forceHR = undefined;
|
||||
this.replay = replay1;
|
||||
this.replay2 = replay2;
|
||||
this.beatmap = beatmap;
|
||||
this.og_beatmap = beatmap.clone();
|
||||
this.og_replay_mods = replay1.info.mods?.clone() as StandardModCombination;
|
||||
this.setMetadata({
|
||||
AR: this.beatmap.difficulty.approachRate,
|
||||
CS: this.beatmap.difficulty.circleSize,
|
||||
OD: this.beatmap.difficulty.overallDifficulty,
|
||||
});
|
||||
}
|
||||
|
||||
static setOptions(beatmap: StandardBeatmap, replay: Score, judgements: Judgements[] ) {
|
||||
this.judgements = judgements;
|
||||
this.forceHR = undefined;
|
||||
@ -457,8 +530,8 @@ export class OsuRenderer {
|
||||
return arScale;
|
||||
}
|
||||
|
||||
private static renderPath() {
|
||||
const frames = this.replay.replay!.frames as LegacyReplayFrame[];
|
||||
private static renderPath(replay: Replay, cursorImage: p5.Image) {
|
||||
const frames = replay.frames as LegacyReplayFrame[];
|
||||
const renderFrames: {
|
||||
position: Vector2;
|
||||
time: number;
|
||||
@ -505,7 +578,7 @@ export class OsuRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
Drawer.drawCursorPath(renderFrames, cursorPushed);
|
||||
Drawer.drawCursorPath(cursorImage, renderFrames, cursorPushed);
|
||||
|
||||
return cursorPushed;
|
||||
}
|
||||
|
||||
@ -73,12 +73,6 @@ export const state = create<{
|
||||
speed: 1
|
||||
}));
|
||||
|
||||
state.subscribe((newState) => {
|
||||
if (newState.beatmap) {
|
||||
document.title = `Viewing replay #${newState.replay?.info.id}`
|
||||
}
|
||||
})
|
||||
|
||||
export let p: p5;
|
||||
|
||||
export function setEnv(_p: p5) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user