From e413b2d76ea293a64fe9ec498f6c207bb5ad8bff Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Mon, 4 Mar 2024 15:26:11 +0100 Subject: [PATCH] Added replay-viewer support for replay pairs --- .../main/kotlin/com/nisemoe/nise/Models.kt | 9 ++ .../nise/controller/ScoreController.kt | 14 +-- .../com/nisemoe/nise/database/ScoreService.kt | 26 +++++- nise-replay-viewer/index.html | 2 +- nise-replay-viewer/public/cursor2.png | Bin 0 -> 13243 bytes nise-replay-viewer/src/interface/App.tsx | 33 ++++--- .../src/interface/composites/Menu.tsx | 11 ++- nise-replay-viewer/src/osu/Drawer.ts | 4 +- nise-replay-viewer/src/osu/OsuRenderer.ts | 85 ++++++++++++++++-- nise-replay-viewer/src/utils.ts | 6 -- 10 files changed, 154 insertions(+), 36 deletions(-) create mode 100644 nise-replay-viewer/public/cursor2.png diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt index c0ecfe9..f7fff2a 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt @@ -102,6 +102,15 @@ data class ReplayViewerData( val judgements: List ) +data class ReplayPairViewerData( + val beatmap: String, + val replay1: String, + val replay2: String, + val mods: Int, + val judgements1: List, + val judgements2: List +) + data class ReplayData( val replay_id: Long, val user_id: Int, diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/ScoreController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/ScoreController.kt index 3fd5581..fd04344 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/ScoreController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/ScoreController.kt @@ -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 { + 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 { val replay1Data = this.scoreService.getReplayData(replay1Id) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt index 1d50cb2..3ea2b3e 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt @@ -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 { 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!!) ) } } diff --git a/nise-replay-viewer/index.html b/nise-replay-viewer/index.html index 6ec771e..5ba5ebe 100644 --- a/nise-replay-viewer/index.html +++ b/nise-replay-viewer/index.html @@ -4,7 +4,7 @@ - /replay/ + /replay/ - nise.moe diff --git a/nise-replay-viewer/public/cursor2.png b/nise-replay-viewer/public/cursor2.png new file mode 100644 index 0000000000000000000000000000000000000000..8f789fe3925a86cf883fab036dec185e4e7c89ba GIT binary patch literal 13243 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 + 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]); diff --git a/nise-replay-viewer/src/interface/composites/Menu.tsx b/nise-replay-viewer/src/interface/composites/Menu.tsx index 2167058..17c5c8b 100644 --- a/nise-replay-viewer/src/interface/composites/Menu.tsx +++ b/nise-replay-viewer/src/interface/composites/Menu.tsx @@ -23,11 +23,16 @@ export function Navbar() { {OsuRenderer.beatmap && ( <> - {" "} + {OsuRenderer.replay2 == null && ( View on nise.moe - + )} + {OsuRenderer.replay2 && ( + + View on nise.moe + )} )} @@ -37,7 +42,7 @@ export function Navbar() { {mods?.map((mod) => { return ( ); diff --git a/nise-replay-viewer/src/osu/Drawer.ts b/nise-replay-viewer/src/osu/Drawer.ts index 3ca9f91..b93bb98 100644 --- a/nise-replay-viewer/src/osu/Drawer.ts +++ b/nise-replay-viewer/src/osu/Drawer.ts @@ -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, diff --git a/nise-replay-viewer/src/osu/OsuRenderer.ts b/nise-replay-viewer/src/osu/OsuRenderer.ts index 0f9a7ca..93de7d7 100644 --- a/nise-replay-viewer/src/osu/OsuRenderer.ts +++ b/nise-replay-viewer/src/osu/OsuRenderer.ts @@ -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; } diff --git a/nise-replay-viewer/src/utils.ts b/nise-replay-viewer/src/utils.ts index 022fc17..21ef2a8 100644 --- a/nise-replay-viewer/src/utils.ts +++ b/nise-replay-viewer/src/utils.ts @@ -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) {