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>
|
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(
|
data class ReplayData(
|
||||||
val replay_id: Long,
|
val replay_id: Long,
|
||||||
val user_id: Int,
|
val user_id: Int,
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
package com.nisemoe.nise.controller
|
package com.nisemoe.nise.controller
|
||||||
|
|
||||||
import com.nisemoe.nise.Format
|
import com.nisemoe.nise.*
|
||||||
import com.nisemoe.nise.ReplayData
|
|
||||||
import com.nisemoe.nise.ReplayPair
|
|
||||||
import com.nisemoe.nise.ReplayViewerData
|
|
||||||
import com.nisemoe.nise.database.ScoreService
|
import com.nisemoe.nise.database.ScoreService
|
||||||
import org.springframework.http.ResponseEntity
|
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.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
@ -32,6 +28,14 @@ class ScoreController(
|
|||||||
return ResponseEntity.ok(replayData)
|
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}")
|
@GetMapping("pair/{replay1Id}/{replay2Id}")
|
||||||
fun getPairDetails(@PathVariable replay1Id: Long, @PathVariable replay2Id: Long): ResponseEntity<ReplayPair> {
|
fun getPairDetails(@PathVariable replay1Id: Long, @PathVariable replay2Id: Long): ResponseEntity<ReplayPair> {
|
||||||
val replay1Data = this.scoreService.getReplayData(replay1Id)
|
val replay1Data = this.scoreService.getReplayData(replay1Id)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.nisemoe.nise.database
|
package com.nisemoe.nise.database
|
||||||
|
|
||||||
|
import com.nisemoe.generated.enums.JudgementType
|
||||||
import com.nisemoe.generated.tables.records.ScoresJudgementsRecord
|
import com.nisemoe.generated.tables.records.ScoresJudgementsRecord
|
||||||
import com.nisemoe.generated.tables.records.ScoresRecord
|
import com.nisemoe.generated.tables.records.ScoresRecord
|
||||||
import com.nisemoe.generated.tables.references.*
|
import com.nisemoe.generated.tables.references.*
|
||||||
@ -48,6 +49,20 @@ class ScoreService(
|
|||||||
.mapNotNull { (scoreType, title) -> db.get(scoreType)?.let { ReplayDataChart(title, it.filterNotNull()) } }
|
.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? {
|
fun getReplayViewerData(replayId: Long): ReplayViewerData? {
|
||||||
val beatmapId = dslContext.select(SCORES.BEATMAP_ID)
|
val beatmapId = dslContext.select(SCORES.BEATMAP_ID)
|
||||||
.from(SCORES)
|
.from(SCORES)
|
||||||
@ -463,6 +478,15 @@ class ScoreService(
|
|||||||
replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
|
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> {
|
fun getJudgements(replayId: Long): List<CircleguardService.ScoreJudgement> {
|
||||||
val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
|
val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
|
||||||
.from(SCORES)
|
.from(SCORES)
|
||||||
@ -486,7 +510,7 @@ class ScoreService(
|
|||||||
distance_center = it.distanceCenter!!,
|
distance_center = it.distanceCenter!!,
|
||||||
distance_edge = it.distanceEdge!!,
|
distance_edge = it.distanceEdge!!,
|
||||||
time = it.time!!,
|
time = it.time!!,
|
||||||
type = CircleguardService.JudgementType.valueOf(it.type!!.literal),
|
type = mapLegacyJudgement(it.type!!)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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">
|
<link rel="icon" type="image/x-icon" href="https://nise.moe/assets/favicon.ico">
|
||||||
<!-- Embed data -->
|
<!-- Embed data -->
|
||||||
<meta property="og:title" content="/nise.moe/ - osu!cheaters finder">
|
<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 {Navbar} from "./composites/Menu";
|
||||||
import {SongSlider} from "./composites/song-slider";
|
import {SongSlider} from "./composites/song-slider";
|
||||||
import {Helper} from "./composites/helper";
|
import {Helper} from "./composites/helper";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect} from "react";
|
||||||
import {OsuRenderer} from "@/osu/OsuRenderer";
|
import {OsuRenderer} from "@/osu/OsuRenderer";
|
||||||
import {Stats} from "@/interface/composites/stats";
|
import {Stats} from "@/interface/composites/stats";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
|
||||||
const [replayId, setReplayId] = useState<number>(0);
|
const loadReplay = async (replayId: number) => {
|
||||||
|
await OsuRenderer.loadReplayFromUrl(replayId);
|
||||||
useEffect(() => {
|
|
||||||
let pathReplayId = Number.parseInt(location.pathname.slice(1, location.pathname.length));
|
|
||||||
|
|
||||||
const loadReplay = async () => {
|
|
||||||
if(document.location.hostname === "localhost") {
|
|
||||||
await OsuRenderer.loadReplayFromUrl(`http://localhost:8080/score/${pathReplayId}/replay`, pathReplayId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await OsuRenderer.loadReplayFromUrl(`https://nise.moe/api/score/${pathReplayId}/replay`, pathReplayId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if(replayId !== pathReplayId) {
|
const loadReplayPair = async (replayId1: number, replayId2: number) => {
|
||||||
setReplayId(pathReplayId);
|
await OsuRenderer.loadReplayPairFromUrl(replayId1, replayId2);
|
||||||
loadReplay();
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
|||||||
@ -23,11 +23,16 @@ export function Navbar() {
|
|||||||
|
|
||||||
{OsuRenderer.beatmap && (
|
{OsuRenderer.beatmap && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{OsuRenderer.replay2 == null && (
|
||||||
<a href={"https://nise.moe/s/" + OsuRenderer.replay.info.id} target="_blank"
|
<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">
|
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
|
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>
|
</div>
|
||||||
@ -37,7 +42,7 @@ export function Navbar() {
|
|||||||
{mods?.map((mod) => {
|
{mods?.map((mod) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={`./mod_${mod.acronym.toLowerCase()}.png`}
|
src={`/mod_${mod.acronym.toLowerCase()}.png`}
|
||||||
className="h-5"
|
className="h-5"
|
||||||
></img>
|
></img>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export class Drawer {
|
|||||||
|
|
||||||
static images = {
|
static images = {
|
||||||
cursor: undefined as any as p5.Image,
|
cursor: undefined as any as p5.Image,
|
||||||
|
cursor2: undefined as any as p5.Image,
|
||||||
cursortrail: undefined as any as p5.Image,
|
cursortrail: undefined as any as p5.Image,
|
||||||
hitcircle: undefined as any as p5.Image,
|
hitcircle: undefined as any as p5.Image,
|
||||||
hitcircleoverlay: undefined as any as p5.Image,
|
hitcircleoverlay: undefined as any as p5.Image,
|
||||||
@ -183,6 +184,7 @@ export class Drawer {
|
|||||||
|
|
||||||
|
|
||||||
static drawCursorPath(
|
static drawCursorPath(
|
||||||
|
cursorImage: p5.Image,
|
||||||
path: {
|
path: {
|
||||||
position: Vector2;
|
position: Vector2;
|
||||||
time: number;
|
time: number;
|
||||||
@ -251,7 +253,7 @@ export class Drawer {
|
|||||||
|
|
||||||
if (cursor.position)
|
if (cursor.position)
|
||||||
Drawer.p.image(
|
Drawer.p.image(
|
||||||
this.images.cursor,
|
cursorImage,
|
||||||
cursor.position.x,
|
cursor.position.x,
|
||||||
cursor.position.y,
|
cursor.position.y,
|
||||||
55,
|
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 {BeatmapDecoder, BeatmapEncoder} from "osu-parsers";
|
||||||
import {
|
import {
|
||||||
Circle,
|
Circle,
|
||||||
@ -15,6 +15,7 @@ import {Vec2} from "@osujs/math";
|
|||||||
import {clamp, getBeatmap, getReplay} from "@/utils";
|
import {clamp, getBeatmap, getReplay} from "@/utils";
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
|
import p5 from "p5";
|
||||||
|
|
||||||
export enum OsuRendererEvents {
|
export enum OsuRendererEvents {
|
||||||
UPDATE = "UPDATE",
|
UPDATE = "UPDATE",
|
||||||
@ -56,6 +57,8 @@ export class OsuRenderer {
|
|||||||
static event = new OsuRendererBridge();
|
static event = new OsuRendererBridge();
|
||||||
|
|
||||||
static judgements: Judgements[] = [];
|
static judgements: Judgements[] = [];
|
||||||
|
static judgements2: Judgements[] = [];
|
||||||
|
|
||||||
static speedMultiplier = 1;
|
static speedMultiplier = 1;
|
||||||
static settings: OsuRendererSettings = {
|
static settings: OsuRendererSettings = {
|
||||||
showCursorPath: true,
|
showCursorPath: true,
|
||||||
@ -73,7 +76,10 @@ export class OsuRenderer {
|
|||||||
|
|
||||||
static beatmap: StandardBeatmap;
|
static beatmap: StandardBeatmap;
|
||||||
static og_beatmap: StandardBeatmap;
|
static og_beatmap: StandardBeatmap;
|
||||||
|
|
||||||
static replay: Score;
|
static replay: Score;
|
||||||
|
static replay2: Score | undefined;
|
||||||
|
|
||||||
static og_replay_mods: StandardModCombination;
|
static og_replay_mods: StandardModCombination;
|
||||||
static forceHR: boolean | undefined = undefined;
|
static forceHR: boolean | undefined = undefined;
|
||||||
|
|
||||||
@ -137,7 +143,11 @@ export class OsuRenderer {
|
|||||||
|
|
||||||
this.lastRender = Date.now();
|
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();
|
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();
|
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, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-NISE-REPLAY': '20240303'
|
'X-NISE-REPLAY': '20240303'
|
||||||
@ -247,6 +304,22 @@ export class OsuRenderer {
|
|||||||
this.event.emit(OsuRendererEvents.LOAD);
|
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[] ) {
|
static setOptions(beatmap: StandardBeatmap, replay: Score, judgements: Judgements[] ) {
|
||||||
this.judgements = judgements;
|
this.judgements = judgements;
|
||||||
this.forceHR = undefined;
|
this.forceHR = undefined;
|
||||||
@ -457,8 +530,8 @@ export class OsuRenderer {
|
|||||||
return arScale;
|
return arScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static renderPath() {
|
private static renderPath(replay: Replay, cursorImage: p5.Image) {
|
||||||
const frames = this.replay.replay!.frames as LegacyReplayFrame[];
|
const frames = replay.frames as LegacyReplayFrame[];
|
||||||
const renderFrames: {
|
const renderFrames: {
|
||||||
position: Vector2;
|
position: Vector2;
|
||||||
time: number;
|
time: number;
|
||||||
@ -505,7 +578,7 @@ export class OsuRenderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Drawer.drawCursorPath(renderFrames, cursorPushed);
|
Drawer.drawCursorPath(cursorImage, renderFrames, cursorPushed);
|
||||||
|
|
||||||
return cursorPushed;
|
return cursorPushed;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,12 +73,6 @@ export const state = create<{
|
|||||||
speed: 1
|
speed: 1
|
||||||
}));
|
}));
|
||||||
|
|
||||||
state.subscribe((newState) => {
|
|
||||||
if (newState.beatmap) {
|
|
||||||
document.title = `Viewing replay #${newState.replay?.info.id}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export let p: p5;
|
export let p: p5;
|
||||||
|
|
||||||
export function setEnv(_p: p5) {
|
export function setEnv(_p: p5) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user