Stuff
This commit is contained in:
parent
9c07baadce
commit
01828a57f1
@ -1,5 +1,6 @@
|
|||||||
package com.nisemoe.nise
|
package com.nisemoe.nise
|
||||||
|
|
||||||
|
import com.nisemoe.nise.integrations.CircleguardService
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@ -97,7 +98,8 @@ data class ReplayDataSimilarScore(
|
|||||||
data class ReplayViewerData(
|
data class ReplayViewerData(
|
||||||
val beatmap: String,
|
val beatmap: String,
|
||||||
val replay: String,
|
val replay: String,
|
||||||
val mods: Int
|
val mods: Int,
|
||||||
|
val judgements: List<CircleguardService.ScoreJudgement>
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ReplayData(
|
data class ReplayData(
|
||||||
|
|||||||
@ -16,7 +16,6 @@ class ScoreController(
|
|||||||
private val scoreService: ScoreService
|
private val scoreService: ScoreService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@CrossOrigin(origins = ["http://wizardly_nash.local"])
|
|
||||||
@GetMapping("score/{replayId}/replay")
|
@GetMapping("score/{replayId}/replay")
|
||||||
fun getReplay(@PathVariable replayId: Long): ResponseEntity<ReplayViewerData> {
|
fun getReplay(@PathVariable replayId: Long): ResponseEntity<ReplayViewerData> {
|
||||||
val replay = this.scoreService.getReplayViewerData(replayId)
|
val replay = this.scoreService.getReplayViewerData(replayId)
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import kotlin.math.roundToInt
|
|||||||
@Service
|
@Service
|
||||||
class ScoreService(
|
class ScoreService(
|
||||||
private val dslContext: DSLContext,
|
private val dslContext: DSLContext,
|
||||||
private val circleguardService: CircleguardService,
|
|
||||||
private val beatmapService: BeatmapService,
|
private val beatmapService: BeatmapService,
|
||||||
private val authService: AuthService,
|
private val authService: AuthService,
|
||||||
private val compressJudgements: CompressJudgements
|
private val compressJudgements: CompressJudgements
|
||||||
@ -66,6 +65,7 @@ class ScoreService(
|
|||||||
return ReplayViewerData(
|
return ReplayViewerData(
|
||||||
beatmap = result.get(BEATMAPS.BEATMAP_FILE, String::class.java),
|
beatmap = result.get(BEATMAPS.BEATMAP_FILE, String::class.java),
|
||||||
replay = String(replay, Charsets.UTF_8).trimEnd(','),
|
replay = String(replay, Charsets.UTF_8).trimEnd(','),
|
||||||
|
judgements = getJudgements(replayId),
|
||||||
mods = mods
|
mods = mods
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -440,6 +440,37 @@ 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 getJudgements(replayId: Long): List<CircleguardService.ScoreJudgement> {
|
||||||
|
val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
|
||||||
|
.from(SCORES)
|
||||||
|
.where(SCORES.REPLAY_ID.eq(replayId))
|
||||||
|
.fetchOneInto(ScoresRecord::class.java)
|
||||||
|
|
||||||
|
if(judgementsRecord?.judgements == null) {
|
||||||
|
val scoreId = dslContext.select(SCORES.ID)
|
||||||
|
.from(SCORES)
|
||||||
|
.where(SCORES.REPLAY_ID.eq(replayId))
|
||||||
|
.fetchOneInto(Int::class.java)
|
||||||
|
|
||||||
|
val judgementRecords = dslContext.selectFrom(SCORES_JUDGEMENTS)
|
||||||
|
.where(SCORES_JUDGEMENTS.SCORE_ID.eq(scoreId))
|
||||||
|
.fetchInto(ScoresJudgementsRecord::class.java)
|
||||||
|
return judgementRecords.map {
|
||||||
|
CircleguardService.ScoreJudgement(
|
||||||
|
x = it.x!!,
|
||||||
|
y = it.y!!,
|
||||||
|
error = it.error!!,
|
||||||
|
distance_center = it.distanceCenter!!,
|
||||||
|
distance_edge = it.distanceEdge!!,
|
||||||
|
time = it.time!!,
|
||||||
|
type = CircleguardService.JudgementType.valueOf(it.type!!.literal),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return compressJudgements.deserialize(judgementsRecord.judgements!!)
|
||||||
|
}
|
||||||
|
|
||||||
fun getHitDistribution(scoreId: Int): Map<Int, DistributionEntry> {
|
fun getHitDistribution(scoreId: Int): Map<Int, DistributionEntry> {
|
||||||
val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
|
val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
|
||||||
.from(SCORES)
|
.from(SCORES)
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import org.springframework.context.annotation.Profile
|
|||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Profile("old_scores")
|
@Profile("fix:scores")
|
||||||
@Service
|
@Service
|
||||||
class FixOldScores(
|
class FixOldScores(
|
||||||
private val dslContext: DSLContext,
|
private val dslContext: DSLContext,
|
||||||
@ -29,12 +29,12 @@ class FixOldScores(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val CURRENT_VERSION = 6
|
const val CURRENT_VERSION = 7
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value("\${OLD_SCORES_WORKERS:4}")
|
@Value("\${OLD_SCORES_WORKERS:4}")
|
||||||
private var workers: Int = 4
|
private var workers: Int = 6
|
||||||
|
|
||||||
@Value("\${OLD_SCORES_PAGE_SIZE:5000}")
|
@Value("\${OLD_SCORES_PAGE_SIZE:5000}")
|
||||||
private var pageSize: Int = 5000
|
private var pageSize: Int = 5000
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class ImportScores(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val CURRENT_VERSION = 6
|
const val CURRENT_VERSION = 7
|
||||||
const val SLEEP_AFTER_API_CALL = 500L
|
const val SLEEP_AFTER_API_CALL = 500L
|
||||||
const val UPDATE_USER_EVERY_DAYS = 7L
|
const val UPDATE_USER_EVERY_DAYS = 7L
|
||||||
const val UPDATE_BANNED_USERS_EVERY_DAYS = 3L
|
const val UPDATE_BANNED_USERS_EVERY_DAYS = 3L
|
||||||
@ -207,7 +207,7 @@ class ImportScores(
|
|||||||
for(topScore in allUserScores) {
|
for(topScore in allUserScores) {
|
||||||
val beatmapExists = dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(topScore.beatmap!!.id))
|
val beatmapExists = dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(topScore.beatmap!!.id))
|
||||||
if (!beatmapExists) {
|
if (!beatmapExists) {
|
||||||
val beatmapFile = this.osuApi.getBeatmapFile(beatmapId = beatmap.id)
|
val beatmapFile = this.osuApi.getBeatmapFile(beatmapId = topScore.beatmap.id)
|
||||||
dslContext.insertInto(BEATMAPS)
|
dslContext.insertInto(BEATMAPS)
|
||||||
.set(BEATMAPS.BEATMAP_ID, topScore.beatmap.id)
|
.set(BEATMAPS.BEATMAP_ID, topScore.beatmap.id)
|
||||||
.set(BEATMAPS.BEATMAPSET_ID, topScore.beatmapset!!.id)
|
.set(BEATMAPS.BEATMAPSET_ID, topScore.beatmapset!!.id)
|
||||||
|
|||||||
1
nise-replay-viewer/.nvmrc
Normal file
1
nise-replay-viewer/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
18.19
|
||||||
BIN
nise-replay-viewer/public/ia-quattro-400-normal.woff2
Normal file
BIN
nise-replay-viewer/public/ia-quattro-400-normal.woff2
Normal file
Binary file not shown.
BIN
nise-replay-viewer/public/ia-quattro-700-normal.woff2
Normal file
BIN
nise-replay-viewer/public/ia-quattro-700-normal.woff2
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
<svg width="16" height="26" viewBox="0 0 16 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="26" viewBox="0 0 16 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6.52637 7.48637V9.63586C6.52637 10.4498 7.18613 11.1095 8.00004 11.1095C8.81395 11.1095 9.47371 10.4498 9.47371 9.63586V7.48637C9.47371 6.67246 8.81395 6.0127 8.00004 6.0127C7.18613 6.0127 6.52637 6.67246 6.52637 7.48637Z" fill="white"/>
|
<path d="M6.52637 7.48637V9.63586C6.52637 10.4498 7.18613 11.1095 8.00004 11.1095C8.81395 11.1095 9.47371 10.4498 9.47371 9.63586V7.48637C9.47371 6.67246 8.81395 6.0127 8.00004 6.0127C7.18613 6.0127 6.52637 6.67246 6.52637 7.48637Z" fill="#373737"/>
|
||||||
<path d="M5.05306 7.4854C5.05306 6.11459 5.9938 4.95933 7.26357 4.63134V0.442383C3.19678 0.815565 0.000976562 4.24509 0.000976562 8.40757V8.52409H5.05311V7.4854H5.05306Z" fill="#373737"/>
|
<path d="M5.05306 7.4854C5.05306 6.11459 5.9938 4.95933 7.26357 4.63134V0.442383C3.19678 0.815565 0.000976562 4.24509 0.000976562 8.40757V8.52409H5.05311V7.4854H5.05306Z" fill="white"/>
|
||||||
<path d="M8.7373 0.442383V4.63129C10.0071 4.95933 10.9478 6.11454 10.9478 7.48535V8.52409H15.9999V8.40757C15.9999 4.24509 12.8041 0.815565 8.7373 0.442383V0.442383Z" fill="#373737"/>
|
<path d="M8.7373 0.442383V4.63129C10.0071 4.95933 10.9478 6.11454 10.9478 7.48535V8.52409H15.9999V8.40757C15.9999 4.24509 12.8041 0.815565 8.7373 0.442383V0.442383Z" fill="#373737"/>
|
||||||
<path d="M8.0004 12.5825C6.49819 12.5825 5.25574 11.4525 5.0762 9.99805H0.000976562V17.5595C0.000976562 21.9704 3.58951 25.559 8.00045 25.559C12.4114 25.559 15.9999 21.9705 15.9999 17.5595V9.99805H10.9246C10.7451 11.4525 9.50266 12.5825 8.0004 12.5825Z" fill="#373737"/>
|
<path d="M8.0004 12.5825C6.49819 12.5825 5.25574 11.4525 5.0762 9.99805H0.000976562V17.5595C0.000976562 21.9704 3.58951 25.559 8.00045 25.559C12.4114 25.559 15.9999 21.9705 15.9999 17.5595V9.99805H10.9246C10.7451 11.4525 9.50266 12.5825 8.0004 12.5825Z" fill="#373737"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 993 B After Width: | Height: | Size: 993 B |
@ -13,13 +13,13 @@ export class CanvasControlHooks {
|
|||||||
static setupCanvasControls() {
|
static setupCanvasControls() {
|
||||||
canvasDragStart = p.createVector(0, 0);
|
canvasDragStart = p.createVector(0, 0);
|
||||||
canvasDragging = false;
|
canvasDragging = false;
|
||||||
canvasMultiplier = p.windowHeight / 384 / 2;
|
canvasMultiplier = p.windowHeight / 384 / 1.5;
|
||||||
canvasTranslation = p.createVector(512, 384 / 3);
|
canvasTranslation = p.createVector(512 / 1.5, 384 / 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Hook(Events.mousePressed)
|
@Hook(Events.mousePressed)
|
||||||
static mousePressed() {
|
static mousePressed() {
|
||||||
if (p.mouseButton === p.CENTER) {
|
if (p.mouseButton === p.LEFT) {
|
||||||
canvasDragging = true;
|
canvasDragging = true;
|
||||||
canvasDragStart = p.createVector(p.mouseX, p.mouseY);
|
canvasDragStart = p.createVector(p.mouseX, p.mouseY);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
import {AboutDialog} from "./composites/about-dialog";
|
import {AboutDialog} from "./composites/about-dialog";
|
||||||
import {AnalysisSheet} from "./composites/analysis.-sheet";
|
|
||||||
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, useState} from "react";
|
||||||
import {OsuRenderer} from "@/osu/OsuRenderer";
|
import {OsuRenderer} from "@/osu/OsuRenderer";
|
||||||
|
import {Stats} from "@/interface/composites/stats";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
|
||||||
const [replayId, setReplayId] = useState<string>("");
|
const [replayId, setReplayId] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let pathReplayId = location.pathname.slice(1, location.pathname.length);
|
let pathReplayId = Number.parseInt(location.pathname.slice(1, location.pathname.length));
|
||||||
|
|
||||||
const loadReplay = async () => {
|
const loadReplay = async () => {
|
||||||
if(document.location.hostname === "localhost") {
|
if(document.location.hostname === "localhost") {
|
||||||
await OsuRenderer.loadReplayFromUrl(`http://localhost:8080/score/${pathReplayId}/replay`);
|
await OsuRenderer.loadReplayFromUrl(`http://localhost:8080/score/${pathReplayId}/replay`, pathReplayId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await OsuRenderer.loadReplayFromUrl(`https://nise.moe/api/score/${pathReplayId}/replay`);
|
await OsuRenderer.loadReplayFromUrl(`https://nise.moe/api/score/${pathReplayId}/replay`, pathReplayId);
|
||||||
};
|
};
|
||||||
|
|
||||||
if(replayId !== pathReplayId) {
|
if(replayId !== pathReplayId) {
|
||||||
@ -31,8 +31,8 @@ export function App() {
|
|||||||
<>
|
<>
|
||||||
<Navbar/>
|
<Navbar/>
|
||||||
<AboutDialog/>
|
<AboutDialog/>
|
||||||
<AnalysisSheet/>
|
|
||||||
<SongSlider/>
|
<SongSlider/>
|
||||||
|
<Stats/>
|
||||||
<Helper/>
|
<Helper/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,82 +1,62 @@
|
|||||||
import {
|
import {Menubar,} from "@/interface/components/ui/menubar";
|
||||||
Menubar,
|
import {OsuRenderer} from "@/osu/OsuRenderer";
|
||||||
MenubarContent,
|
import {state} from "@/utils";
|
||||||
MenubarItem,
|
|
||||||
MenubarMenu,
|
|
||||||
MenubarTrigger,
|
|
||||||
} from "@/interface/components/ui/menubar";
|
|
||||||
import { OsuRenderer } from "@/osu/OsuRenderer";
|
|
||||||
import { state } from "@/utils";
|
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const { beatmap, mods } = state();
|
const {beatmap, mods} = state();
|
||||||
return (
|
return (
|
||||||
<Menubar className="rounded-none border-x-0 border-t-0 flex justify-between px-4">
|
<Menubar className="rounded-none border-x-0 border-t-0 flex justify-between px-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img src={"https://nise.moe/assets/keisatsu-chan.png"} width={48}/>
|
<img src={"https://nise.moe/assets/keisatsu-chan.png"} width={48}/>
|
||||||
<h3 className="scroll-m-20 text-lg font-semibold tracking-tight">
|
<h3 className="scroll-m-20 text-lg font-semibold tracking-tight">
|
||||||
Replay Viewer
|
/replay/
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
|
||||||
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>File</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() => {
|
|
||||||
state.setState({ aboutDialog: true });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
|
|
||||||
{OsuRenderer.beatmap && (
|
|
||||||
<>
|
|
||||||
{" "}
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>Analyzer</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() => {
|
|
||||||
state.setState({ dataAnalysisDialog: true });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
gRDA
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{beatmap && (
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
{mods?.map((mod) => {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={`./mod_${mod.acronym.toLowerCase()}.png`}
|
|
||||||
className="h-5"
|
|
||||||
></img>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div className="flex flex-col items-end">
|
|
||||||
<p className="text-xs opacity-75">Currently Viewing</p>
|
|
||||||
|
|
||||||
<p className="text-xs font-bold">
|
|
||||||
{beatmap?.metadata.artist} - {beatmap?.metadata.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<img
|
|
||||||
src={`https://assets.ppy.sh/beatmaps/${beatmap?.metadata.beatmapSetId}/covers/cover.jpg?${beatmap?.metadata.beatmapId}`}
|
<button onClick={() => {
|
||||||
alt=""
|
state.setState({aboutDialog: true});
|
||||||
width={90}
|
}}
|
||||||
className="rounded-sm"
|
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
|
||||||
/>
|
About
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{OsuRenderer.beatmap && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</Menubar>
|
{beatmap && (
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
{mods?.map((mod) => {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={`./mod_${mod.acronym.toLowerCase()}.png`}
|
||||||
|
className="h-5"
|
||||||
|
></img>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<p className="text-xs opacity-75">Currently Viewing</p>
|
||||||
|
|
||||||
|
<p className="text-xs font-bold">
|
||||||
|
{beatmap?.metadata.artist} - {beatmap?.metadata.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={`https://assets.ppy.sh/beatmaps/${beatmap?.metadata.beatmapSetId}/covers/cover.jpg?${beatmap?.metadata.beatmapId}`}
|
||||||
|
alt=""
|
||||||
|
width={90}
|
||||||
|
className="rounded-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Menubar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Button } from "@/interface/components/ui/button";
|
|
||||||
import { Dialog, DialogContent } from "@/interface/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/interface/components/ui/dialog";
|
||||||
import { Badge } from "@/interface/components/ui/badge";
|
|
||||||
import { state } from "@/utils";
|
import { state } from "@/utils";
|
||||||
|
|
||||||
export function AboutDialog() {
|
export function AboutDialog() {
|
||||||
const { aboutDialog } = state();
|
const { aboutDialog } = state();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
} from "@/interface/components/ui/sheet";
|
|
||||||
import { Button } from "@/interface/components/ui/button";
|
|
||||||
import { BarChart, XAxis, Bar, ResponsiveContainer } from "recharts";
|
|
||||||
import { state } from "@/utils";
|
|
||||||
import { gRDA } from "@/osu/Analysis";
|
|
||||||
import { OsuRenderer } from "@/osu/OsuRenderer";
|
|
||||||
|
|
||||||
export function AnalysisSheet() {
|
|
||||||
const { dataAnalysisDialog, grda } = state();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sheet
|
|
||||||
open={dataAnalysisDialog}
|
|
||||||
onOpenChange={(opened: boolean) => {
|
|
||||||
state.setState({ dataAnalysisDialog: opened });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SheetContent
|
|
||||||
side="left"
|
|
||||||
className="min-w-[600px] overflow-scroll overflow-x-hidden SCROLL"
|
|
||||||
>
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle>gRDA</SheetTitle>
|
|
||||||
<SheetDescription>
|
|
||||||
Most of the information provided here is forensic and can be used to
|
|
||||||
detect specific types of cheats, such as Timewarp and Relax. This
|
|
||||||
process might take up to a minute to do and collect all information.
|
|
||||||
</SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="mt-4"
|
|
||||||
onClick={() => {
|
|
||||||
state.setState({
|
|
||||||
grda: gRDA(OsuRenderer.replay, OsuRenderer.beatmap),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Analyze Replay
|
|
||||||
</Button>
|
|
||||||
{grda && (
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold tracking-tight flex items-center gap-3 mt-6">
|
|
||||||
<span>Response</span>
|
|
||||||
</h3>
|
|
||||||
<div className="opacity-75">
|
|
||||||
<h3 className="text-sm">Frametime Averages:</h3>
|
|
||||||
<ResponsiveContainer height={200} width="100%">
|
|
||||||
<BarChart height={200} data={Object.entries(grda.frameTimes)}>
|
|
||||||
<XAxis dataKey="0" />
|
|
||||||
<Bar dataKey="1" fill="#8884d8" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
|
|
||||||
<div>Normalization rate due to mods {grda.normalizationRate}</div>
|
|
||||||
<ResponsiveContainer height={200} width="100%">
|
|
||||||
<BarChart
|
|
||||||
height={200}
|
|
||||||
data={Object.entries(grda.moddedFrameTimes)}
|
|
||||||
>
|
|
||||||
<XAxis dataKey="0" />
|
|
||||||
<Bar dataKey="1" fill="#8884d8" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="opacity-75">
|
|
||||||
<h3 className="text-sm">Averages:</h3>
|
|
||||||
<div>
|
|
||||||
Slider Delta Hold Average{" "}
|
|
||||||
{Math.round(
|
|
||||||
(grda.sliderDeltaHoldAverage / grda.sliderLength) * 100
|
|
||||||
) / 100}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Approximated circle hold delta range ={" "}
|
|
||||||
{grda.circleExtremes.max - grda.circleExtremes.min}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Approximated slider hold delta range ={" "}
|
|
||||||
{grda.sliderExtremes.max - grda.sliderExtremes.min}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>Circle | Holdtime distribution</div>
|
|
||||||
<ResponsiveContainer height={400} width="100%">
|
|
||||||
<BarChart
|
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
|
||||||
height={400}
|
|
||||||
data={Object.entries(grda.holdCircleDistributionGraph)}
|
|
||||||
>
|
|
||||||
<XAxis dataKey="0" />
|
|
||||||
<Bar dataKey="1" fill="#8884d8" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
|
|
||||||
<div>Circle | Press time distribution</div>
|
|
||||||
<ResponsiveContainer height={400} width="100%">
|
|
||||||
<BarChart
|
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
|
||||||
height={400}
|
|
||||||
data={Object.entries(grda.pressCircleDistributionGraph).sort(
|
|
||||||
(a, b) => Number(a[0]) - Number(b[0])
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<XAxis dataKey="0" />
|
|
||||||
<Bar dataKey="1" fill="#8884d8" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import middlemouse from "/mouse.svg";
|
import leftmouse from "/mouse.svg";
|
||||||
import scroll from "/scroll.svg";
|
import scroll from "/scroll.svg";
|
||||||
import space from "/space.svg";
|
import space from "/space.svg";
|
||||||
import { state } from "@/utils";
|
import { state } from "@/utils";
|
||||||
@ -12,7 +12,7 @@ export function Helper() {
|
|||||||
<div className="flex flex-col absolute right-10 top-16 gap-2 items-end">
|
<div className="flex flex-col absolute right-10 top-16 gap-2 items-end">
|
||||||
<div className="flex gap-2 text-xs items-center">
|
<div className="flex gap-2 text-xs items-center">
|
||||||
Drag Playfield
|
Drag Playfield
|
||||||
<img src={middlemouse}/>
|
<img src={leftmouse}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 text-xs items-center">
|
<div className="flex gap-2 text-xs items-center">
|
||||||
Zoom
|
Zoom
|
||||||
|
|||||||
17
nise-replay-viewer/src/interface/composites/stats.tsx
Normal file
17
nise-replay-viewer/src/interface/composites/stats.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { state } from "@/utils";
|
||||||
|
import {OsuRenderer} from "@/osu/OsuRenderer";
|
||||||
|
|
||||||
|
export function Stats() {
|
||||||
|
const { replay } = state();
|
||||||
|
|
||||||
|
if (!replay) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col absolute left-10 top-16 gap-2">
|
||||||
|
<div className="flex gap-2 text-xs">
|
||||||
|
cvUR: {OsuRenderer.cvUR}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
@ -24,6 +24,14 @@ export enum OsuRendererEvents {
|
|||||||
SETTINGS = "SETTINGS",
|
SETTINGS = "SETTINGS",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Judgements {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
time: number;
|
||||||
|
error: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OsuRendererSettings {
|
export interface OsuRendererSettings {
|
||||||
showCursorPath: boolean;
|
showCursorPath: boolean;
|
||||||
showFutureCursorPath: boolean;
|
showFutureCursorPath: boolean;
|
||||||
@ -41,18 +49,27 @@ export class OsuRenderer {
|
|||||||
private static fadeIn: number;
|
private static fadeIn: number;
|
||||||
private static lastRender: number = Date.now();
|
private static lastRender: number = Date.now();
|
||||||
|
|
||||||
|
|
||||||
static playing: boolean = false;
|
static playing: boolean = false;
|
||||||
|
|
||||||
static event = new OsuRendererBridge();
|
static event = new OsuRendererBridge();
|
||||||
|
|
||||||
|
static judgements: Judgements[] = [];
|
||||||
static speedMultiplier = 1;
|
static speedMultiplier = 1;
|
||||||
static settings: OsuRendererSettings = {
|
static settings: OsuRendererSettings = {
|
||||||
showCursorPath: true,
|
showCursorPath: true,
|
||||||
showFutureCursorPath: true,
|
showFutureCursorPath: false,
|
||||||
showKeyPress: true,
|
showKeyPress: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static cvUR = 0;
|
||||||
|
static totalMisses = 0;
|
||||||
|
static totalThreeHundreds = 0;
|
||||||
|
static totalHundreds = 0;
|
||||||
|
static totalFifties = 0;
|
||||||
|
|
||||||
static time: number = 0;
|
static time: number = 0;
|
||||||
|
|
||||||
static beatmap: StandardBeatmap;
|
static beatmap: StandardBeatmap;
|
||||||
static og_beatmap: StandardBeatmap;
|
static og_beatmap: StandardBeatmap;
|
||||||
static replay: Score;
|
static replay: Score;
|
||||||
@ -68,6 +85,40 @@ export class OsuRenderer {
|
|||||||
this.beatmap = undefined as any;
|
this.beatmap = undefined as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cvUR of the current replay, rounded to 2 decimals.
|
||||||
|
*/
|
||||||
|
static updateStatistics() {
|
||||||
|
let pastJudgements = this.judgements.filter(j => j.time < this.time);
|
||||||
|
if(pastJudgements.length === 0) {
|
||||||
|
this.cvUR = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate 300x, 100x, 50x, Misses
|
||||||
|
this.totalMisses = pastJudgements.filter(j => j.type == "MISS").length;
|
||||||
|
this.totalThreeHundreds = pastJudgements.filter(j => j.type == "THREE_HUNDRED").length;
|
||||||
|
this.totalHundreds = pastJudgements.filter(j => j.type == "ONE_HUNDRED").length;
|
||||||
|
this.totalFifties = pastJudgements.filter(j => j.type == "FIFTY").length;
|
||||||
|
|
||||||
|
// UR is equal to the standard deviation of all the hit errors
|
||||||
|
let hitErrors = pastJudgements.map(j => j.error);
|
||||||
|
let mean = hitErrors.reduce((a, b) => a + b, 0) / hitErrors.length;
|
||||||
|
let variance = hitErrors.map(e => Math.pow(e - mean, 2)).reduce((a, b) => a + b, 0) / hitErrors.length;
|
||||||
|
let UR = Math.sqrt(variance) * 10;
|
||||||
|
|
||||||
|
// To obtain the cvUR, we need to account for mods like DT and HT
|
||||||
|
if(this.beatmap.mods.bitwise & 64) {
|
||||||
|
UR /= 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.beatmap.mods.bitwise & 256) {
|
||||||
|
UR /= 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cvUR = Math.round(UR * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
static render() {
|
static render() {
|
||||||
if (!this.beatmap || !this.replay) return;
|
if (!this.beatmap || !this.replay) return;
|
||||||
|
|
||||||
@ -80,6 +131,7 @@ export class OsuRenderer {
|
|||||||
|
|
||||||
if (this.playing) {
|
if (this.playing) {
|
||||||
this.setTime(this.time + ((Date.now() - this.lastRender) * this.speedMultiplier));
|
this.setTime(this.time + ((Date.now() - this.lastRender) * this.speedMultiplier));
|
||||||
|
this.updateStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastRender = Date.now();
|
this.lastRender = Date.now();
|
||||||
@ -163,7 +215,7 @@ export class OsuRenderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static async loadReplayFromUrl(url: string) {
|
static async loadReplayFromUrl(url: string, replayId: number) {
|
||||||
OsuRenderer.purge();
|
OsuRenderer.purge();
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@ -174,19 +226,21 @@ export class OsuRenderer {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const { beatmap, replay, mods } = data;
|
const { beatmap, replay, mods, judgements } = data;
|
||||||
|
|
||||||
const i_replay = await getReplay(replay);
|
const i_replay = await getReplay(replay);
|
||||||
|
i_replay.info.id = replayId;
|
||||||
i_replay.info.rawMods = mods;
|
i_replay.info.rawMods = mods;
|
||||||
i_replay.info.mods = new StandardModCombination(mods);
|
i_replay.info.mods = new StandardModCombination(mods);
|
||||||
|
|
||||||
const i_beatmap = await getBeatmap(beatmap, i_replay);
|
const i_beatmap = await getBeatmap(beatmap, i_replay);
|
||||||
|
|
||||||
OsuRenderer.setOptions(i_beatmap, i_replay);
|
OsuRenderer.setOptions(i_beatmap, i_replay, judgements);
|
||||||
this.event.emit(OsuRendererEvents.LOAD);
|
this.event.emit(OsuRendererEvents.LOAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
static setOptions(beatmap: StandardBeatmap, replay: Score) {
|
static setOptions(beatmap: StandardBeatmap, replay: Score, judgements: Judgements[] ) {
|
||||||
|
this.judgements = judgements;
|
||||||
this.forceHR = undefined;
|
this.forceHR = undefined;
|
||||||
this.replay = replay;
|
this.replay = replay;
|
||||||
this.beatmap = beatmap;
|
this.beatmap = beatmap;
|
||||||
@ -222,7 +276,7 @@ export class OsuRenderer {
|
|||||||
private static calculateEffects(hitObject: StandardHitObject) {
|
private static calculateEffects(hitObject: StandardHitObject) {
|
||||||
let vEndTime = hitObject.startTime;
|
let vEndTime = hitObject.startTime;
|
||||||
|
|
||||||
if (hitObject instanceof Slider || hitObject instanceof Spinner) {
|
if (hitObject instanceof Spinner) {
|
||||||
vEndTime = hitObject.endTime + 25;
|
vEndTime = hitObject.endTime + 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,9 +305,6 @@ export class OsuRenderer {
|
|||||||
this.time > hitObject.startTime - this.preempt &&
|
this.time > hitObject.startTime - this.preempt &&
|
||||||
this.time < vEndTime + hitObject.hitWindows.windowFor(HitResult.Meh);
|
this.time < vEndTime + hitObject.hitWindows.windowFor(HitResult.Meh);
|
||||||
|
|
||||||
if (hitObject instanceof Slider && this.time > hitObject.endTime) {
|
|
||||||
opacity -= (this.time - hitObject.endTime) / 25;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
opacity,
|
opacity,
|
||||||
arScale,
|
arScale,
|
||||||
@ -261,6 +312,63 @@ export class OsuRenderer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static calculateSliderEffects(hitObject: Slider) {
|
||||||
|
let sliderCircleEndTime = hitObject.startTime;
|
||||||
|
let vEndTime = hitObject.endTime + 25;
|
||||||
|
|
||||||
|
const sliderCircleFadeOut = Math.max(
|
||||||
|
0.0,
|
||||||
|
(this.time - sliderCircleEndTime) / 375
|
||||||
|
);
|
||||||
|
|
||||||
|
const fadeOut = Math.max(
|
||||||
|
0.0,
|
||||||
|
(this.time - vEndTime) / hitObject.hitWindows.windowFor(HitResult.Meh)
|
||||||
|
);
|
||||||
|
|
||||||
|
let sliderCircleOpacity = Math.max(
|
||||||
|
0.0,
|
||||||
|
Math.min(
|
||||||
|
1.0,
|
||||||
|
Math.min(
|
||||||
|
1.0,
|
||||||
|
(this.time - hitObject.startTime + this.preempt) / this.fadeIn
|
||||||
|
) - sliderCircleFadeOut
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let opacity = Math.max(
|
||||||
|
0.0,
|
||||||
|
Math.min(
|
||||||
|
1.0,
|
||||||
|
Math.min(
|
||||||
|
1.0,
|
||||||
|
(this.time - hitObject.startTime + this.preempt) / this.fadeIn
|
||||||
|
) - fadeOut
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const arScale = Math.max(
|
||||||
|
1,
|
||||||
|
((hitObject.startTime - this.time) / this.preempt) * 3.0 + 1.0
|
||||||
|
);
|
||||||
|
|
||||||
|
let visible =
|
||||||
|
this.time > hitObject.startTime - this.preempt &&
|
||||||
|
this.time < vEndTime + hitObject.hitWindows.windowFor(HitResult.Meh);
|
||||||
|
|
||||||
|
if (this.time > hitObject.endTime) {
|
||||||
|
opacity -= (this.time - hitObject.endTime) / 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
opacity,
|
||||||
|
sliderCircleOpacity,
|
||||||
|
arScale,
|
||||||
|
visible,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static renderCircle(hitObject: Circle) {
|
private static renderCircle(hitObject: Circle) {
|
||||||
if (hitObject.startTime > this.time + 10000) return;
|
if (hitObject.startTime > this.time + 10000) return;
|
||||||
const { arScale, opacity, visible } = this.calculateEffects(hitObject);
|
const { arScale, opacity, visible } = this.calculateEffects(hitObject);
|
||||||
@ -297,7 +405,7 @@ export class OsuRenderer {
|
|||||||
private static renderSlider(hitObject: Slider) {
|
private static renderSlider(hitObject: Slider) {
|
||||||
if (hitObject.endTime > this.time + 10000) return;
|
if (hitObject.endTime > this.time + 10000) return;
|
||||||
|
|
||||||
const { arScale, opacity, visible } = this.calculateEffects(hitObject);
|
const { arScale, opacity, sliderCircleOpacity, visible } = this.calculateSliderEffects(hitObject);
|
||||||
|
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
Drawer.beginDrawing();
|
Drawer.beginDrawing();
|
||||||
@ -308,7 +416,7 @@ export class OsuRenderer {
|
|||||||
hitObject.path.path,
|
hitObject.path.path,
|
||||||
hitObject.radius
|
hitObject.radius
|
||||||
);
|
);
|
||||||
Drawer.setDrawingOpacity(opacity);
|
Drawer.setDrawingOpacity(sliderCircleOpacity);
|
||||||
|
|
||||||
Drawer.drawApproachCircle(
|
Drawer.drawApproachCircle(
|
||||||
hitObject.stackedStartPosition,
|
hitObject.stackedStartPosition,
|
||||||
|
|||||||
@ -1,3 +1,19 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'iA Writer Quattro';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(ia-quattro-400-normal.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'iA Writer Quattro';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(ia-quattro-700-normal.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
margin:0;
|
margin:0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -6,6 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#root{
|
#root{
|
||||||
|
font-family: 'iA Writer Quattro', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body{
|
body{
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export const state = create<{
|
|||||||
|
|
||||||
state.subscribe((newState) => {
|
state.subscribe((newState) => {
|
||||||
if (newState.beatmap) {
|
if (newState.beatmap) {
|
||||||
document.title = `${newState.beatmap.metadata.artist} - ${newState.beatmap.metadata.titleUnicode} | Replay Inspector`
|
document.title = `Viewing replay #${newState.replay?.info.id}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user