From 40854f24731996cc0dea49419b4f74dcc49a9381 Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Sun, 3 Mar 2024 15:28:26 +0100 Subject: [PATCH] Integrating new replay viewer --- .../main/kotlin/com/nisemoe/nise/Models.kt | 5 +- .../nisemoe/nise/config/CustomCorsFilter.kt | 18 + .../nise/controller/ScoreController.kt | 2 + .../com/nisemoe/nise/database/ScoreService.kt | 21 +- .../nise/integrations/CircleguardService.kt | 99 ---- nise-circleguard/src/main.py | 142 +---- nise-frontend/src/app/app.component.ts | 1 - nise-frontend/src/app/replays.ts | 65 --- .../app/view-score/view-score.component.html | 5 - .../app/view-score/view-score.component.ts | 20 +- .../assets/replay-viewer/approachcircle.png | Bin 4504 -> 0 bytes .../src/assets/replay-viewer/cursor.png | Bin 22772 -> 0 bytes .../src/assets/replay-viewer/hitcircle.png | Bin 5560 -> 0 bytes .../assets/replay-viewer/hitcircleoverlay.png | Bin 14720 -> 0 bytes .../src/assets/replay-viewer/reversearrow.png | Bin 6211 -> 0 bytes .../src/assets/replay-viewer/sliderball.png | Bin 10899 -> 0 bytes .../components/replay-viewer/decode-replay.ts | 45 -- .../replay-viewer/process-replay.ts | 81 --- .../replay-viewer/replay-service.ts | 497 ------------------ .../replay-viewer/replay-viewer.component.css | 39 -- .../replay-viewer.component.html | 43 -- .../replay-viewer/replay-viewer.component.ts | 108 ---- .../components/replay-viewer/sample-replay.ts | 224 -------- 23 files changed, 44 insertions(+), 1371 deletions(-) delete mode 100644 nise-frontend/src/assets/replay-viewer/approachcircle.png delete mode 100644 nise-frontend/src/assets/replay-viewer/cursor.png delete mode 100644 nise-frontend/src/assets/replay-viewer/hitcircle.png delete mode 100644 nise-frontend/src/assets/replay-viewer/hitcircleoverlay.png delete mode 100644 nise-frontend/src/assets/replay-viewer/reversearrow.png delete mode 100644 nise-frontend/src/assets/replay-viewer/sliderball.png delete mode 100644 nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts delete mode 100644 nise-frontend/src/corelib/components/replay-viewer/process-replay.ts delete mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-service.ts delete mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.css delete mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.html delete mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.ts delete mode 100644 nise-frontend/src/corelib/components/replay-viewer/sample-replay.ts 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 5bb495e..a986d04 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt @@ -1,6 +1,5 @@ package com.nisemoe.nise -import com.nisemoe.nise.integrations.CircleguardService import kotlinx.serialization.Serializable import java.time.OffsetDateTime @@ -96,9 +95,9 @@ data class ReplayDataSimilarScore( ) data class ReplayViewerData( + val beatmap: String, val replay: String, - val beatmap: CircleguardService.BeatmapResponse, - val mods: List + val mods: Int ) data class ReplayData( diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/config/CustomCorsFilter.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/CustomCorsFilter.kt index 521f659..d82064b 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/config/CustomCorsFilter.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/CustomCorsFilter.kt @@ -1,20 +1,38 @@ package com.nisemoe.nise.config import jakarta.servlet.* +import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.beans.factory.annotation.Value +import org.springframework.security.web.header.HeaderWriterFilter class CustomCorsFilter : Filter { @Value("\${ORIGIN:http://localhost:4200}") private lateinit var origin: String + @Value("\${REPLAY_ORIGIN:http://localhost:5173}") + private lateinit var replayOrigin: String + override fun init(filterConfig: FilterConfig) { // We don't really need to do anything special here. } override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) { val response = servletResponse as HttpServletResponse + val request = servletRequest as HttpServletRequest + + if(response.containsHeader("X-NISE-REPLAY") || request.getHeader("origin") == replayOrigin) { + response.setHeader("Access-Control-Allow-Origin", replayOrigin) + response.setHeader("Access-Control-Allow-Methods", "GET") + response.setHeader( + "Access-Control-Allow-Headers", + "Access-Control-Allow-Headers, Origin,Accept, X-NISE-REPLAY, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers" + ) + filterChain.doFilter(servletRequest, servletResponse) + return + } + response.setHeader("Access-Control-Allow-Origin", origin) response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS,PATCH") response.setHeader( 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 4e83765..b929f02 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 @@ -6,6 +6,7 @@ import com.nisemoe.nise.ReplayPair import com.nisemoe.nise.ReplayViewerData 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 @@ -15,6 +16,7 @@ class ScoreController( private val scoreService: ScoreService ) { + @CrossOrigin(origins = ["http://wizardly_nash.local"]) @GetMapping("score/{replayId}/replay") fun getReplay(@PathVariable replayId: Long): ResponseEntity { val replay = this.scoreService.getReplayViewerData(replayId) 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 ef87501..ef5ad07 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 @@ -55,27 +55,26 @@ class ScoreService( .where(SCORES.REPLAY_ID.eq(replayId)) .fetchOne() ?: return null - var replayData = result.get(SCORES.REPLAY, String::class.java) ?: return null + val replayData = result.get(SCORES.REPLAY, String::class.java) ?: return null - val decompressedReplay = Base64.getDecoder().decode(replayData).inputStream().use { byteStream -> - LZMACompressorInputStream(byteStream).readBytes() - } - replayData = String(decompressedReplay, Charsets.UTF_8).trimEnd(',') + val replay = decompressData(replayData) if(result.get(BEATMAPS.BEATMAP_FILE, String::class.java) == null) return null val mods = result.get(SCORES.MODS, Int::class.java) - val beatmapFile = result.get(BEATMAPS.BEATMAP_FILE, String::class.java) - val beatmapData = this.circleguardService.processBeatmap(beatmapFile, mods = mods).get() - return ReplayViewerData( - replay = replayData, - beatmap = beatmapData, - mods = Mod.parseModCombination(mods) + beatmap = result.get(BEATMAPS.BEATMAP_FILE, String::class.java), + replay = String(replay, Charsets.UTF_8).trimEnd(','), + mods = mods ) } + private fun decompressData(replayString: String): ByteArray = + Base64.getDecoder().decode(replayString).inputStream().use { byteStream -> + LZMACompressorInputStream(byteStream).readBytes() + } + fun getReplayData(replayId: Long): ReplayData? { val result = dslContext.select( SCORES.ID, diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt index 7f96637..4360ed5 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt @@ -106,105 +106,6 @@ class CircleguardService { replayResponse.error_skewness = replayResponse.error_skewness?.times(conversionFactor) } - @Serializable - data class BeatmapRequest( - val beatmap_file: String, - val mods: Int - ) - - @Serializable - data class Circle( - val x: Double, - val y: Double, - val time: Double, - val new_combo: Boolean, - val current_combo: Int, - val combo_color: String - ) - - @Serializable - data class SliderCurvePoint( - val x: Double, - val y: Double - ) - - @Serializable - data class SliderCurve( - val type: String, - val points: List - ) - - @Serializable - data class Slider( - val x: Double, - val y: Double, - val time: Double, - val end_time: Double, - val curve: SliderCurve, - val length: Double, - val new_combo: Boolean, - val current_combo: Int, - val combo_color: String, - val repeat: Int - ) - - @Serializable - data class Spinner( - val x: Double, - val y: Double, - val time: Double, - val end_time: Double, - val new_combo: Boolean, - val current_combo: Int, - val combo_color: String - ) - - @Serializable - data class Difficulty( - val hp_drain_rate: Double, - val circle_size: Double, - val overral_difficulty: Double, - val approach_rate: Double, - val slider_multiplier: Double, - val slider_tick_rate: Double - ) - - @Serializable - data class BeatmapResponse( - val circles: List, - val sliders: List, - val spinners: List, - val difficulty: Difficulty, - val audio_lead_in: Double, - ) - - fun processBeatmap(beatmapFile: String, mods: Int): CompletableFuture { - val requestUri = "$apiUrl/beatmap" - - val request = BeatmapRequest( - beatmap_file = beatmapFile, - mods = mods - ) - - // Serialize the request object to JSON - val requestBody = serializer.encodeToString(BeatmapRequest.serializer(), request) - - val httpRequest = HttpRequest.newBuilder() - .uri(URI.create(requestUri)) - .header("Content-Type", "application/json") // Set content type to application/json - .POST(HttpRequest.BodyPublishers.ofString(requestBody)) - .build() - - return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) - .thenApply { response: HttpResponse -> - if (response.statusCode() == 200) { - serializer.decodeFromString(BeatmapResponse.serializer(), response.body()) - } else { - throw RuntimeException("Failed to process beatmap: ${response.body()}") - } - } - } - fun processReplay(replayData: String, beatmapData: String, mods: Int = 0): CompletableFuture { val requestUri = "$apiUrl/replay" diff --git a/nise-circleguard/src/main.py b/nise-circleguard/src/main.py index d798f45..6c1adae 100644 --- a/nise-circleguard/src/main.py +++ b/nise-circleguard/src/main.py @@ -42,137 +42,14 @@ def my_filter_outliers(arr, bias=1.5): arr_without_outliers = [x for x in arr if lower_limit < x < upper_limit]; return arr if not arr_without_outliers else arr_without_outliers -@dataclass -class BeatmapRequest: - beatmap_file: str - mods: int - - @staticmethod - def from_dict(data): - try: - return BeatmapRequest( - beatmap_file=data['beatmap_file'], - mods=data['mods'] - ) - except (ValueError, KeyError, TypeError) as e: - raise ValueError(f"Invalid data format: {e}") - - -@app.post("/beatmap") -def process_beatmap(): - try: - request_data = request.get_json() - if not request_data: - abort(400, description="Bad Request: No JSON data provided.") - - beatmap_request = BeatmapRequest.from_dict(request_data) - - cg_beatmap = Beatmap.parse(beatmap_request.beatmap_file) - - circles = [] - sliders = [] - spinners = [] - - def map_slider_curve_type(slider: Slider): - if str(type(slider.curve)) == "": - return 'Linear' - if str(type(slider.curve)) == "": - return 'Perfect' - elif str(type(slider.curve)) == "": - return 'MultiBezier' - elif str(type(slider.curve)) == "": - return 'CatMull' - - combo_colors = { - 1: "255,81,81", # Combo1 - 2: "255,128,64", # Combo2 - 3: "128,64,0", # Combo3 - 4: "212,212,212" # Combo4 - } - - current_combo = 1 # Initialize current combo counter - combo_counter = 0 # Keep track of how many combos have been counted - - hit_objects = cg_beatmap.hit_objects( - easy=bool(beatmap_request.mods & Mod.Easy.value), - hard_rock=bool(beatmap_request.mods & Mod.HardRock.value), - half_time=bool(beatmap_request.mods & Mod.HalfTime.value), - double_time=bool(beatmap_request.mods & Mod.DoubleTime.value), - ) - - for obj in hit_objects: - if obj.new_combo: - combo_counter += 1 # Increment combo counter - if combo_counter > 4: - combo_counter = 1 # Reset combo counter after 4 - current_combo = 1 # Reset current combo to 1 on new combo - else: - current_combo += 1 # Increment current combo for each hit object - - # Assign combo color based on the combo_counter - combo_color = combo_colors[combo_counter] - - if obj.type_code & Circle.type_code: - circles.append({ - "x": obj.position.x, - "y": obj.position.y, - "time": obj.time.total_seconds() * 1000, - "new_combo": obj.new_combo, - "combo_color": combo_color, - "current_combo": current_combo - }) - elif obj.type_code & Slider.type_code: - slider: Slider = obj - sliders.append({ - "x": slider.position.x, - "y": slider.position.y, - "time": slider.time.total_seconds() * 1000, - "end_time": slider.end_time.total_seconds() * 1000, - "curve": { - 'type': map_slider_curve_type(slider), - 'points': [{'x': p.x, 'y': p.y} for p in slider.curve.points] - }, - "length": slider.length, - "new_combo": slider.new_combo, - "combo_color": combo_color, - "current_combo": current_combo, - "repeat": slider.repeat, - }) - elif obj.type_code & Spinner.type_code: - spinner: Spinner = obj - spinners.append({ - "x": spinner.position.x, - "y": spinner.position.y, - "time": spinner.time.total_seconds() * 1000, - "end_time": spinner.end_time.total_seconds() * 1000, - "new_combo": spinner.new_combo, - "combo_color": combo_color, - "current_combo": current_combo - }) - - # Reset current_combo if new_combo is True - if obj.new_combo: - current_combo = 1 - - return jsonify( - { - "circles": circles, - "sliders": sliders, - "spinners": spinners, - "difficulty": { - "hp_drain_rate": cg_beatmap.hp(), - "circle_size": cg_beatmap.cs(), - "overral_difficulty": cg_beatmap.od(), - "approach_rate": cg_beatmap.ar(), - "slider_multiplier": cg_beatmap.slider_multiplier, - "slider_tick_rate": cg_beatmap.slider_tick_rate - }, - "audio_lead_in": cg_beatmap.audio_lead_in.total_seconds() * 1000, - } - ) - - except ValueError as e: - abort(400, description=str(e)) +def remove_bom_from_first_line(beatmap_file): + lines = beatmap_file.splitlines() + if lines: # Check if there are lines to avoid index errors + # Remove BOM only from the first line + lines[0] = lines[0].replace('\ufeff', '') + # Join the lines back together + clean_content = '\n'.join(lines) + return clean_content @dataclass @@ -261,7 +138,8 @@ def process_replay(): result_bytes1 = memory_stream1.getvalue() replay1 = ReplayString(result_bytes1) - cg_beatmap = Beatmap.parse(replay_request.beatmap_data) + clean_beatmap_file = remove_bom_from_first_line(replay_request.beatmap_data) + cg_beatmap = Beatmap.parse(clean_beatmap_file) ur = cg.ur(replay=replay1, beatmap=cg_beatmap) adjusted_ur = cg.ur(replay=replay1, beatmap=cg_beatmap, adjusted=True) diff --git a/nise-frontend/src/app/app.component.ts b/nise-frontend/src/app/app.component.ts index 5defd07..4655b96 100644 --- a/nise-frontend/src/app/app.component.ts +++ b/nise-frontend/src/app/app.component.ts @@ -3,7 +3,6 @@ import {Router, RouterLink, RouterOutlet} from "@angular/router"; import {UserService} from "../corelib/service/user.service"; import {NgIf} from '@angular/common'; import {FormsModule} from '@angular/forms'; -import {ReplayViewerComponent} from "../corelib/components/replay-viewer/replay-viewer.component"; @Component({ selector: 'app-root', diff --git a/nise-frontend/src/app/replays.ts b/nise-frontend/src/app/replays.ts index 6c7e416..e9fdadc 100644 --- a/nise-frontend/src/app/replays.ts +++ b/nise-frontend/src/app/replays.ts @@ -13,71 +13,6 @@ export interface ReplayDataSimilarScore { correlation: number; } -interface Circle { - x: number; - y: number; - time: number; - new_combo: boolean; - current_combo: number; - combo_color: string; -} - -interface SliderCurvePoint { - x: number; - y: number; -} - -export interface SliderCurve { - type: string; - points: SliderCurvePoint[]; -} - -export interface Slider { - x: number; - y: number; - time: number; - end_time: number; - curve: SliderCurve; - length: number; - new_combo: boolean; - current_combo: number; - combo_color: string; - repeat: number; -} - -interface Spinner { - x: number; - y: number; - time: number; - end_time: number; - new_combo: boolean; - current_combo: number; - combo_color: string; -} - -interface Difficulty { - hp_drain_rate: number; - circle_size: number; - overral_difficulty: number; - approach_rate: number; - slider_multiplier: number; - slider_tick_rate: number; -} - -interface BeatmapResponse { - circles: Circle[]; - sliders: Slider[]; - spinners: Spinner[]; - difficulty: Difficulty; - audio_lead_in: number; -} - -export interface ReplayViewerData { - replay: string; - beatmap: BeatmapResponse; - mods: string[]; -} - export interface ReplayData { replay_id: number; user_id: number; diff --git a/nise-frontend/src/app/view-score/view-score.component.html b/nise-frontend/src/app/view-score/view-score.component.html index a9b6426..8ceac98 100644 --- a/nise-frontend/src/app/view-score/view-score.component.html +++ b/nise-frontend/src/app/view-score/view-score.component.html @@ -201,9 +201,4 @@ class="chart"> - -
-

# replay viewer (experimental)

- -
diff --git a/nise-frontend/src/app/view-score/view-score.component.ts b/nise-frontend/src/app/view-score/view-score.component.ts index f9cabd6..9dbe4e4 100644 --- a/nise-frontend/src/app/view-score/view-score.component.ts +++ b/nise-frontend/src/app/view-score/view-score.component.ts @@ -6,12 +6,11 @@ import {environment} from "../../environments/environment"; import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common"; import {ActivatedRoute, RouterLink} from "@angular/router"; import {catchError, throwError} from "rxjs"; -import {DistributionEntry, ReplayData, ReplayViewerData} from "../replays"; +import {DistributionEntry, ReplayData} from "../replays"; import {calculateAccuracy} from "../format"; import {Title} from "@angular/platform-browser"; import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component"; import {ChartComponent} from "../../corelib/components/chart/chart.component"; -import {ReplayViewerComponent} from "../../corelib/components/replay-viewer/replay-viewer.component"; @Component({ selector: 'app-view-score', @@ -25,8 +24,7 @@ import {ReplayViewerComponent} from "../../corelib/components/replay-viewer/repl NgOptimizedImage, RouterLink, OsuGradeComponent, - ChartComponent, - ReplayViewerComponent + ChartComponent ], templateUrl: './view-score.component.html', styleUrl: './view-score.component.css' @@ -39,7 +37,6 @@ export class ViewScoreComponent implements OnInit { isLoading = false; error: string | null = null; replayData: ReplayData | null = null; - replayViewerData: ReplayViewerData | null = null; replayId: number | null = null; public barChartLegend = true; @@ -77,10 +74,6 @@ export class ViewScoreComponent implements OnInit { this.replayId = params['replayId']; if (this.replayId) { this.loadScoreData(); - - if(this.activatedRoute.snapshot.queryParams['viewer'] === 'true') { - this.loadReplayViewerData(); - } } }); } @@ -99,15 +92,6 @@ export class ViewScoreComponent implements OnInit { return url; } - private loadReplayViewerData(): void { - this.http.get(`${environment.apiUrl}/score/${this.replayId}/replay`) - .subscribe({ - next: (response) => { - this.replayViewerData = response; - } - }); - } - private loadScoreData(): void { this.isLoading = true; this.http.get(`${environment.apiUrl}/score/${this.replayId}`).pipe( diff --git a/nise-frontend/src/assets/replay-viewer/approachcircle.png b/nise-frontend/src/assets/replay-viewer/approachcircle.png deleted file mode 100644 index 56d6d34c1abf6ab153c9cef62eecff3016d21ed9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4504 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
8LRaKg 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 LNkvXXu0mjf2Xb$Z diff --git a/nise-frontend/src/assets/replay-viewer/hitcircle.png b/nise-frontend/src/assets/replay-viewer/hitcircle.png deleted file mode 100644 index 8259a975087d2bfc9e7e8a338a8e659d72246aa7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5560 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�A~>+KNEX79zN32%>B&h-;P*4 z_d8vvzlrL+rXpCJl&b_tJAD@RggBnE@KN>bXc&YNi*dHhB@U@Z+@6^aD?IA!yUqVK zqVQ*{hdGg}csQJ+m_JRsc+FP1;31E`fk9hktV&S4^5Xqe>MIfHEibumt2>CjEfAAJwV`M|c>3IZQCzP2rw+Qd~(BDu(B5T<*{$ z4(fk(I{WNi$oV%(G?7rhScazh21``Rk?hNet)0YFBTHBxRp{`^BYE{Z40^#14(sX@ z0KuNbkLD;*6Js()^=(^wc9`xnqNhp?=oSBLChPG!mm9-g(d1&EN2|MC5xk7rI>Xm4 zQ%W)771MGQFPTJDHm!tQ>}M$u)U>u{+FDz;x#{JH-cmuAcB>QDRX}_IXAhOlE~zQX z7@sj%-6+`cYk-+K7+TZN&~QC%Zl}N&;nm7GFUnMp0+$Jby+3|O8I9cV6tfIv&h4vBT- zN{Wk$@yL~V#w8~V3=~ekZa1xlT(}#&FQkNg zZs%`nskZ3|lu+^lO&-hToXNwZfOU_nJFX-v1F|(cI~!WRzqa#J5{r}nkmu1n(BqHt|=gb3L3X*rM{ zr_e47eQy0weM&?s^ikW3{mwDh&eyhxI0zfNNyW5v!p7y24Hvo5Q}t)FA~f~$KPkpu zMiVRVg_-cB`VM+?#h^Y-g<*8qi&{2X%Z+{nEn>=y$N!S&x6pqR#7$fO)hrOvJTbAe za`{rwBW@DK-rC&K(z3j;5LJSQT8{IXrQV{bwfKzVmA}X?#)Lx^oHx34)OK+-*#O$w z&@-c_fmaUVOu^L3e7bpM_vpl}_70E#T*fbQgdDy68H|^o64~*hfsOrEMT135B2*gb z0gu3Hz!9tNo}PBA%Ri%QNFK6iv|H`PzIRq2N4_}YX|gTUqxBq`Y(|{e)>^dBFSJH6 zpr>n5WY`-cZPN(KV=hSqlcBgNSfz>WH)^YrFaZaRsZX==gtU1506*?LzB>(;5-U)7 z+OJ(~bZm4v2}LDuRv3-Qqc;#vezC~DNgIK*(awe}(+t6OL-6QL=bm2xtPhV}788^? zr-N$`ha-G}l4-^C$JL%JRkpvi4}1wopQ*;+ff0=duc(ykd5h0$jmk{gEEPQ-<>us& zDFF(5sDDGF1Yuoe;1KpJXV;=g7PzHmvf~ruO{~Jw0=XHg9rdz^aVtokd3JXdK|_@ zvPMirdzt?$oQ*Cvo*!*SSP!{js>S9pp+dVs)-)T8Ko7N~;qP&l(|Y;L~RhxUCM za4rq&b25RXfKBGe)mPERb;9UwL_-+c%#p&Y#RqwGAac=FpFV!uTIy39cp_kTAIQa5 zv;Vd_a0GXIncf+9{bwWzple8Q_y62wobjb6c=CDeAE?RX<*hXaT59@;DplmG{{hNV BVFmyI diff --git a/nise-frontend/src/assets/replay-viewer/hitcircleoverlay.png b/nise-frontend/src/assets/replay-viewer/hitcircleoverlay.png deleted file mode 100644 index 0b47e9f6c5638af6fa0a93be783b2a22e29e6fca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14720 zcmW++1yodB7k$9cDIg$S3ewWujewMNDJfFY>44JGA1xtBcXx@Dv`D9Pr!X}C8diX_x zQooWx5SF^FjLfT7Hjb{2E;f!%v`R8Ev`!x!t!&?0LXhWdx|X%J);5V4V)37}N+|q4 z6-Uj-7_^$w5pd$zXDoD>1j?aw-)0_bbUu=kgHiNlg`z}7!M{A#V8aQ=nnquv%Z~b# z6B_>O`maxp-DC@5W9Y7SN_?~AG`)HRy#oU$QISW34~|nPP5o&3Q}557ziS+l0a%Pq z5CKND1)a++0}^!MFDlAP-+|r+A$g8tV?Z6MX&qdYA@}$@(kVtr@XtscuCanD7;s`p z(mP5z50aEef~O}x(}J>KklDc7H>=PqcF2tB^i1WmI;TuQoDicm#8)G^tpf_w;Yxtlq7U6gx>m&|g>ou{$=-YI3*; z8r=Ra`{@D9#soXd4H02gg)dnPWo^&uo;p3)M($fo9>cI+|rNx0Fs! zs^&t$H|NkhCY!fspOVsCybNZklG+@%eP#c9xWtLt0j%< z{vOy?*h<<$`R9Ry8~pfFZ>Tl%lQc4PGV3QEWaY^(^PY@HJn7Zu5_qwjyqSWZ;;5s? zQpe)^IQAD-f^x#M1PT^T?ZTqGqQxR?ol~6w?dU=iEx$5(9UiUKLd-vj#X3dluXeR4 zw5$t73POsk3mmm2zWeLnhN0Z*ph$()~b>L0#HeE59jIYl1t z%f=U$Ms`JsNr`&Jip3hm3cIcRW&8Q*d98BI`qu$?QoYJdMobFU(Zi_en_4qk3;9_2 zB&uSoc}1lQ7vArIAhYftKzt>zpk+UNyYj! z+FV(keVw1V^&3*_`tGu>gZS2Xzb3!PTQPLpkQuyUDg!Eimzod6!mouFQ-{AENceUC zOWfwP)*B%hndVuoBN}-A(>7fzZCt^c)Vk5-(`jf#m`dOPN0K;48b!}SU+bE(>zT9h zU+>>%>1owzKlTge$`AMtY!5=_GY5GypJ(XvxQur7$t-KEk4a!>CUDs;mH4a6s0Ucr zZZdGb<&4!WovNtM^(aD^ACiini+{*m6<-wXNeItbo>$H6OMS^|lzJ=`9atK;-dRd#)3+B> zN{84?9*!Nh#S41nzco3EZoXk2(dCcB6ciGq8!Qp!6sku)_9BVTV{Bf=Wl8o*Hc@sl z&H9D9P&vCYAB%Vq$2-wkfhOK2i5?Cs{lN*nbUmf_DwIMTEM@!-@7tDc_D@4r(pRuo zeMplz`;48uz77BQ7nDy@$U(>^oGAG+R88=|BPjxw0+KIJPl!C8MqzpVp!#7=&pF0F z7CJ3s#6qohqR67^9k&gcUpUHG2zx1pWZJ3wv<;Z2SkSAiEp@Hh zhr)gdB^GC}4($(C|C);t_YsovIDp6;zIjq&ksI0-S z(Mm)c!s}S4*PB71g?L7%iTw8inNFf8=W+`o>a}P6X`bSrj`qv z+V8mTh~CAItM@j4ZB8v~G_q?v@NTX3u|jN9WC#o|{A=|(Cb^%S$Pf}+Xg&3IM06w0 zmPi)-KQP@64>ZqsFddY(sJEE@liZh@ky!|tkTATPpFW)b_^!hv>vU{h0f83uY2+^U zEE-=1H994FAQ(4zH$zZdNN7dk!|nD>zRZZl$g>R6yX?DxGEx#w|BH#;so}BYv2RHy zN%8IWzPYzM9lKA;=X!qhP`CAa^1Lwl_xgIj(_`L)>M&;sUwzG&_9OGX&1L18)@c59 z{FC_jdl*Xc>CIan_BntzY2PYpszQ+WQ-DK1LD1zrc-@8|H!cX;F@qrCWC$X4j5F<) zhaiq(C0QwL&)GdcuVmu0bVU6@QCU%2vvshuS+LZBOrwGnF>*4Q5Pz7yYmU+vhYo=z zFVdW6J)*@blP%OUs$a|oa~y~y~dO`Ki&N-V8Q(2g*2q~>}T4sQ87@7QCnh&{L0gDMv402XRnr> zNq3(;GE-WsxyyThUr)Wy*kOcWCJZmPyqhF&(wW;$-M>$bP$m;oeaw*x9edscO0b4! z6CG_S&35EbQa{h1Vp7kbq!xCyX{f5IDrjgBq{M*v2GWL7_IlZ*T0IZXcBbq_f^fRH zv~_fd}JA!+lp)_2Q};-wvJqE7&c@D;mCU()>7yk9 zgtYX$fPtzc4GH2u{fY_&Z+hM3OWbZZP&hQ0OUuc1zbc=i*v6COJP+-D6UoQNcU|wW z&~)8?x7&V~@$y;xlcRN+Pm`yDxP!}Lt((B94E zX~=N8h{vxA^RAxIY_i5baFdrl$DS>fO-)xhI>oQ;^{XuU?{F?OH#FoGzfBu`PRD=~ zcJ8MJb}o~uz|YU!r|!L4O$Mb7_DM!)CmjNYk)Z2Ao{^U+FW&Zi0sF+e&W2i(>)zSf z6$`wduJHf+Bgl}4G3LF6qP1Ex{aQm%Vj{`-zOeMSS0juNa@)d)2$EyK0UgGVYK#83 zaQ|RhXDib ze)#0y-p*58k8)T!+#NwW@>R%Xqr{9$Z+1bFEcjb-5leAV>T4aH>h)!^s8vC?1GDGh zZ#WXk)G{4yZG~F>Z?EbPn-AOEo)BhPR1kG);9}ttVB_L7CqQ`1hHV*DsO}j!7ow0S zwa;E<(VS!ZZQaVs3KFT^x757n_V)IwLqkK>JG;BPHSRA z;|LP=*;qPBZkfRU*v+)=IQqLfF)_hZTwF}yL^*{KIQjyI2fX3Q4=_*0gy81MVdlxUsm~H^ z(FfM4`d~rdgxbMmYpXCWYgS%SXcko7y;B&xg1GuCJKAR&QBID%k3OkSm zql%a93Y({==4k|*6Qt&ieS4+&6-3;!Mvgr0?o{~_0kB^i0X=hgobUC1eG-RW#j^2d z81p0IG;*?_nB}TR9Z0^#S*sRjKN_4@%e?O{H%YZ$zmD22!nX%bD&e{{%4Asg{?cZ1 zIGvAJE>|f|8(h!BHJ^w7R*d_@9gQZZAsn@erWA)3wfano;91n_3{e<9*X|e>&PyGg zc`h6_8OTUSM|zxBT1SWbRI#u~*)~%x~1#f6+n`6@_bjFg=8G0adhogfKpQ7Ba6{&J+eeKW&n<5YC%sj2tIV9Hw& zkAwL-;2p+-Q7SO00$$a8+tk-KsjtEPN$?n0s!|*;S@4)`>ZE?+1QKME%I`!e@Fe&J z1S!VxB0(5|Ts&3Lwy9Q)&`S+sN=QZ=)zl!?)X@R{3N8a39eEv{ zjaj?${p*Bs-}Rd~>~b6tgp^s4 z>xmOLiJ3Cq{H8lxBu_2f+fX;Kc|2d8W)@h#2P7Cj>G7JE8rrGm$-G@$${T<55_Q1x zg`&op!@2bmC%}R|Gp4{+O5?Mc!NnsJif3BJtbA@#P!z5d_t`ErSl?PWz2~VCJ+WM; zrT#Mu{b%6)_%`p;0qz8~{B2j^A?ENO;6nQ(bzk3pyHCONGMTHERhCPYh~IJXX4C6* zgMBEbgFY~UKhM4}`#?5OMHwO;Q0N>n5K|prjHD0{7qsY$DUx1qtE{Yazr8xMTk_s5 z#kWmOd=eb1mzeM*SVJ&M3#=O~+A~8J+`;9QF{Pi#Cm6*8w&9f_0IYAD%Ks#|{}MUT zl4^!&fZM8g=Pv~HTM+6JquMz-GV!W*S?Kp@6%xlPGAimPhFNec<&N8OCn%3qvGIUw z+cj17S^P^bGMu|*bcqJ9z3IyF7dTy5Pdht5p?eH}6*{%m(Rs_y7h|N^O-X3PjqHjn z&T_d3qTC?*;GtxprR~T`n z9*#o)=;Wk)U)7i1H!rwJA~XuM!@u;dx^kyjXciK)DiN{{ROQ#!CbaFAH9uSWFu=a_ zhD@IMGYnM1J5X>w`gR55D)2Cdio{6Pq6aUiK}j~5()50v$oGazSCgapnf_N2f>EdL z8;2mcCrk8C^QTOF>?sDSloBnUW^8H&()LkDD;!_n+GcFlnsF6i1!i5}`loMt$q>t} zY6h4v)9qQ<*xNrfFfi!TW0QYIhzS-sM)D}GpOHz4Fi9<6OxM^=XS)@#C{k|P?$3bb z98svvQs?ydNAIH7X8I=#OhUzAGfo;5V!_#hVps=dhXVVO0LT!X_oou~rJe-m!65mOaxX=`5M-OgY4PA?fXh#!|v6$f{jcO#1vU~DDUf~Bo~2=IR)3!sIz zkBHuI|U9Q5k%(rP*17 z!(&fElX8K-i0k-HvsdA=^dFRz!YJ`VCRSG7)PDaidwP8<&rHmX8~`^*8m=l)iZdN- zcpb-B%@k}-ixH?q0OP`Wdm`$1B24Ox-ldI;KVrIG6i!8*7klk){O-D@i>3x;ah$F1|P&hK7FjB z++q(G&SaVK^(@W(MK3}qDgrw&hFI>!I^z0i`%-3(V<4$@|F6QB}6XV<(d2vq0@8SV(;1V14mA+is>#WuC)WPQN&<}~xZ zC35r9U5X&izg^-9R*>djCp`{!_Jo|V0swjV+Ls(Je8oW^#Ag>!TOS882j*;+dZYB6tDiSjAK|H}QMdx?@?yp>-9(YEinEIK<~ zhDN%Ey!Shr#Jg(p9+WFTYmg{;()H{n#9of`CUO`ztu1&gEexIdg7cH5B~bj_^;esh zJV8ZS`S+NOHg-r?Q}5~vKk-cGT>aKG!%<$`>Urd-)eh&cz=@PJ{xc^-TNZRkPU+&p z(QEg=%>XdhRXNa%%X6Z3%0#8ar1dJRG`>%~kGGt4>kD+A(Ms9=|FktVlqTR95I-Uv zVKR`!`O$eOm9JThT77=^tOq4PlAAD0am9vuH>DTH;G%4cWdcRD0ce=16Y@NNZdMkY>3m!=@48tLQ5&n+!2Sy(Q0 zt0qVZ8LeG%{=;x`z4kFpWQYs53mWHDHJ*9_?s6K-tI-mRA3@zxXT%6}R>{W<%qlJ_VlnYM zo50Unz4@tTjHCfS%eicWphfFUzw=!pCbgTUaa8?f8gXAwgqFJ488}Baao zqAXTO*ET1^g&Lz|zgcVt7Y;aB9#tYxuO?<^XWzcf{#KopLkwJD;)6jOtyDOrcljCdSIm9xI>(ZUXve z%AOHqWL^moTZUSt%Tl z=wkD?-s0c*N?i7Ee={as!(ZVY3Gg5aQT!+;hyUdg`!$d3&eX#TDgy@i)ZHOED$~`MlEZ zJVQJJMyWcU@z5oey{uUA%n)`f@}8-(G%Sgt${b+=A98heb{3|@vjZspzUo)oocRI@ z$Oj;mqahiLLb&t)h08|ORxTDsoxstOgtMn43VZQ1Z4!|!FPg`lc-W$_ZnKQ z2d|Ke)jGJo6f$F3rKh}zkOf&TJ3Ct%1jb`8jCFPn4u=_VNX~ufsScLtLM-sj<9q#G zQaO#U!frbP3netrL-V3%Y-(ycQdRQP`72xa8({(rX8(3mY=A*PrZ3@`H#tgAfUgSj zXyL#3@wb8mFC+mJ4C@|wMYe%FP>_QJo+`Z?$VcX}8oc0_(`ZpeEh$6@?jXNHr3FOn zuj|(KtBG6arG>i@RM&HnX~tlG1gaYFbpsH4>sTes8ZRu|@gg9u`{_UhUWxXg5$udZ~Xz5Vb zGeg}+lp3^=u7u~4AC#CMBMju=YO~5 z<>e8z-`^hUTI}2?0Z#zsQ-~O=5ab7PgOyuug&R_Gd;r!`@-Vku$#sc?K)6ovM*e5n z=r>(qPIOuTafYvKgNOYIJS}-k1ihT^M73c0KnUwpP2=C+Rhx8tWCnn`<2WySH5==x zXy*C#?KXg20OP0_ejbO08|YD zR0v82$6OkUtTzJ>;t6ho<|mK4AV|b+vP9Vq9ym_QD;e znM4VIN8QDk=r#La-1>vM)^&*31%5pA<%=3v4Oo*Sz!CQ10tAur=!>A0KzYw7IWC2B zhEA3E{<24)?Zv{-6%ypWH+`%-a3vI_C99zw*qOD1xONLR|A6LjPEAUJ4=QGtCQ`;G zU&iKR71TP1g)4nF5|MayRI8B;v4uQ{F&j!A*M1!tfUn?y@LwO9$LV0Z~p`OMo*gP0GX)L?V%)VzfA^YiIKt^D&ft$;8hrq@ESz!|cRwsQ`U zfS!X z4q(V?{6Uo(6^t6^hAAFSSLPTbT27sC^*RSMx@_XPW5KAV4$=gjR|!xdB=+(paR){at@-P9*#KaRp{`~Ch?01#r0Qu^1!`hj5b*E|hnQ3a*=BZ`= zS4)x~uK*bU*o$aNAx-;UY_VUzeg)IQXkU@WRJW*TXv7lWpbab#XXWK>&}tfJ&&|yx zfF7RjS$w}N=TTCduuUUUqUG7(SD8pgrS)-j39;|CsVl*TfR7`Mlt(qhfqOV12`u@Z zP7uo-J6zreprGSP_EJCdTWs+vtzxT3NV{!E4`MM-Rtb_T#TjU6X#pa4chdou#T*WJ zkgE<~4mb<~g{-RFkfEu}uG!;I9-Vy6^@WMc5Z^xA0Kc`#t{6uwp+#Ol1y0viN_Qo1 zYF_Qe#zy=(BIn}laH)OpId-4^SheF)TY>`&^eVU@y9i=a=@v;_f9r!rF*cvEoKr90 zh<9;4QyqUcPz9(Pz}pDRX^=;)dcS?U`h;mheg&>ll6qrq*6qi?fE@>t8vb3!1;Jp=n+3GgpIwBaKhT2I74s07r zi&1v@@!`2!4O*|=2OGP*-TJ?ffA{&eyV$jQOh(zeorpyM`!+E-Iayf5(iq=Mw6oOy z@n;z{Q??u)9!?0*toy)IKpO5)m9$kqRpNzgf?e$9{uN0HQ1Pl7+R<7i{jdkLUJ~Q; z4Xcj8j*wpi11iuq;qXi@DFQ1)Q{_`VZsJwS7U18?5QJOmKhP)U z$$Q)lPYAcb;WYY@baByx54{>x`cC`>J8;=8&Fc*y)l5wtnIv_m^2WF;17Mq0*D4~IS^L5su=xyK!fxthmnfr^h}jJ4TOJCLBaWIdmh#k~uPSb#4GBav3pCqx*= za5ElB_NFMehNwJ)mwCKr2N)}vZr}sUQg{v9fcL9Gc4X-59N#B5El72|CO8=FiE!D= z^+Y6M@2wQ*#f-}5GFG@8WOwN!r}6tW%ht&gesq^zOvnr-_3H8s)X>Mb-J}RBo8oiz z*Hc-v{_+Pb%GRzecw8#o(2ld&)Hhl;T$6kf305Ngy74ar%vUq!u+$yV`)XS+CyjK#wVA-FwP;6T_%<4Y0_n z-BeiuQP^ivNR(t0wfWJXaJPlg(+21|ad+Ah*7b{A@iUI;B=K(`c!02}K3|Jhmj*Os ztv2VIy@QJ6yfF7aDU9^%d3l9pA0k!-JGpRT)$+$0KTliMDYC~4G17XZ&2V;o<>7k^ z)6~~b!Q0$)++gOhEY06tL31_M?c^8wBb&1RS{}e?7IyZt26z;m9hPaCO#e?T+!Rl` z46l6}+<$@)FI~Gv^>ePKCj!H#zLU7q>a6JsFPO#x)r8a48jUgKG>pJ0`SwQBC~PLb z72A^67r;(mcxh_Blht);zfoGcGAQbtoDOTC#aQ&^p#9`b;zBsAd+G`-RbVuIqR<4= znRwp(Y13XA zU}d4e0Ub7bTFFEh2BX4&BbNK-WgWP@$Jq$8sx2_)4tL596_T7L9z>y$%kbH8?$%LbbJ`>?*kJgbV3!0uID{tP3sexw`!6pja79k2FM z#P?f7Fo#FMFhaU=%_y|2bm1f7*Ci@Jp)Ku4^mC1}nF5^a9;6pS+9_2sS7`XuMt~2u z8%QcJio1N5i3s^_UY1J5+9Y= z8_GiUbGW%UJ};n&H0`^)WxZa%ZmX-SyK418)FE@{5!ItJ<*TI&Z~P~L{MGQ&K`@0w zL519ekV;g;dX__glJ4-q%}tR1`HBB-vSgGKbOHe_M7@hz!zC#q;nF@Zgg}A%Pel!ljOLfTGqrp`6p5hLX)F{V z+dx?k(-nU!vYD0YVe zMV~_|v^n?c?~#y!jEqcnZtl8;l2ZRGChYz3{E;jhUKU^a5Yq*fU1T^{y4VOIO@D!N9_3QXdCn^NL0<5yVdRtFVG>rtF zaG7XAHMtvS)&hbGXnn&>_k-V+;*~)!g|=q*cB;%cQ~DqWnUUA4#C4MT z9ESbU z*boGulD575zDYeAaJYM)rRzn{UF4H)9)L?B8Xg+ajv?!)AnvFrgkwS5x~6_-Z}r6m27Ye;xH{h#IRS3O{K=)Odao=C zo8@R7xrTMKFP7m2=}~pgw+$dRGMG|SvGtmGZ|ACW!?r5QK55}(A;aSo8RGy0HnZ(s z4MgCBmeb+#fYZ1&{l{cHRydQ`%xITvHIlnbbRG=&?m*Cz10-HXa``@ZIRHei($6cA zpHQ9_Ee70+9yLCYq-#S`|Vu2 zUpk`_i>ZiX+vNsY+KpYGAdIN~%-k+TAP-5>e>2@9il3L4(!Kgsc=j|pjjt2Q*QM`$ zZRAhg3ynJykZp8lsrlcNSw-CJFZ+P{FTL6uotaU-e|2@$#j0B-n)>8rYF(u8uO0}_ z43T4iM=sXbj*DD!xc~T!DUg(u6v6|mIMlXmL~htfwX5J{%_2=Xcbo3Sg+!wz?k*(8 zc!xIZtO28TINfo`McAM5=9QCwjv^-ngJ^5~uFpwfQlU)=oD#SH#&H2VGj#|89-3Xg zA?AGY!J`NcqC9m$gL?nn^D|{zpQGi@f`2*J{D53Xfp>rdVZHkY^yEZZj9;buo}!Cf zR)fhOgIpq9YUq;{F&=$*0}(l+6y<_h2YbK`=rA?z=z@lEl4e-_U@gWoD{HnR!Qxv| z(LT-vG~8UTpXPZ2rxH{21pKL|a&tIu8i16W={{c6O?h>9^6UGTI%hB=|Tq z;*{4_`XAd6vmN!}>}eDOsk?ap5CU z?Om=)(w*7L&#?M6yXM;3vs1ti-Y~CT?;j_>VM03K)<-Hu?|br0NhOKH?>Wr?kl#Mu z?wT%THagi|k`Cu8!em15y31hLt^ZB^)!ea+DG5i1{vLq7Iu;~XDc!Z0bp}rTy?GORJSg{#^uN|*@Ys}l z=C&5rLjap4HnLiHX|MT_;q5IgEhFIR*GtsVy=mq9z=7}mcK#Hq(>m+TCq}wCA*rcU zlGP2$$2rZ6@IOX*fj$?C8Jyv zj3D$9@u5Xs6R-7N(Gj21+R|O`E1;#Aenk?sjJR`S+V!A^hs8+N=YA%H7!ij{rigPu z6wS{>`z;5c`vWC%VniN!xYS70X<2e_Am&_0TieXb`(jiqO%6I@H*LGQe*E|`gRq^) zM3D~X;aq(=UfB75xlebd{0lWHPzCpwmwH)Ka1)>I+@`AO7$vNgo4dK`w9S6mIvLjX*CKy2eE*|QG;Wjr0AdZM^6e@Bi9Vl_#ov~3@q^Ly zfV~j^_@v4-9f=^={=(tM`|JCImn48WMK3q3KQND&cw$TR-YpO>CkzyiO4b>~T#s6V z+d-E267o_#(1nx`^DZOz&%f_)W`4YD2h#rt{dU~~V$*m6ch>@=DxS4s^7}Eeb}&lv~f9FHh9z2@@)ES&R0wBfqqyh;?3ZuH6EDIJwILuh=xRQojuY zYI{hBzaLbek1ud`T}yu=mOkE(+@EmXpD6r1*`G-){zobeG3iRqY0A(*7r?aleG|H- zVwKg{;$A0fi=aclXCaBGrDc>{|7BA7tLbw*lX|Ky7E)Q$4$EL(m6{Jv&KUTfMWzhT z6;_WWKZGCZGBQZti*Bot0^+tRwefg`?u&d>*mQ8MOV6Xdfy8N-Q!JVOF>sf12QBgU`2jei=Y)hcn-g_*y z`DKd0CaUZQO-+t~UoAW)=-VRZ8)#)Uc*C5x zi8XHKEThgYMU?cuV1i*I7pfUWB>nfF86E<|BgU7+Ia3tEc#vHm#%NH1ATve$!8new zcFAqZ7~g)TYCV~KL1scLgkx`YG{H1_MwpGcCVEygu-tFo4&~!%+R{y&Iay^4&Dod& zjeV+s<06e_p*DqPx3bU|i?I%`dO`>i^t-C@YSf01795VL^4>QG+^^EygOM*(Yyje0 zze$&IrmOXWOnX1xezVRQVxO4#p`ehsCDBC}bNP>@ZMlgTu*hWRi0J9yD#$*%eM1fa zlGS=1uOyLjn%q>PV2)~8gZ>Xw^Qy}%!mctQb%KPZQk?kk$gqQ!qy`bhKUM-X|ARg?JvgsXN06jg5ewoSMcyA{N{*eymG=Q_mNC0(+y!jdW`vD#S zX~#gnptiQYeyZH4@uW0_#@~~HkW3kmqw|2o^}CZLQ^2hkT|0jD?WAu?8jeCExXAvnf{HrAc-(NU+p%=^pCdzZt-)_i+gm!eU?L(xiFGcFSlh7!wNP6BDz z3DkKV%h}Cd8+^f3OXWp72|uBjF6rf zHZ%`-CS_``wOB*oqIp_C+LQ~z1N5KELor|tCv;It;v~O`po5Ev{iITa_n@u-(&?aK zJ1@;)_uY{j_^(@Fc9ZL$MCyB0&!uMR`1=%K@L_@G4x<~Nkf-_t%g=xAJb}yrueFFU zy+1R}S5M~pD<9p<;C|NQ0KUE3zJ8(ZPv?@zS>_fg)?GJY2T+kMhfBzB_N*nE??4I6 z7&&ceFP1GRFPEsapQ{_7~}dfasA?NR|w)gXwIh&{SnRTK%tN0;OL}g+(BiQvnC6f@fD)s5xR_5?MkM`>Y zIv{#ca{FE!Y2@bSCQQB-A=T6ZOn*O^s%gFJqPaUVHk4uu`pp6+z5@xg%g4 zkaOavz-;wRF&(N%Rm)&RU;`XJ3pL-nKRST;V=FExDPdz_VL83@jXfyfp#;NSQ#sM% zSHB*pUjy`I%Mdfp-JUm|RE3ISyF-^C2{?!tJ zb@Lbhqo6A&G+M#96#VkU-F>8}HgpQ2)Q(`B_RePgSAro4r%4OV@JLA) z%8vty?9Cr92Tbq8QeNi8RJqNPOgElcCI=b#Mbk?f8X9&&K(fg9clAN)Uo5tYMe-YV zqU;?8;c)@_<6wL;F0)ra3KSZsT3LSuDa8>2Vw))na0)h{s9!%UQ$3E+C(OVZG7gyM zdqSl(F5P+f=z;V#zWWl1(V+50a@Gj8o<%q_H83#H4d4Yn8Q(i?FF856c11=_Fy`_j zn;GqNKRfnt)Xi9hk&wMoucQzRbRB?d&xF{U(W>@7+sOfO=iIk&tm4e9;_*6tFK*uS z5o{}b4Q$sp94>QkV6t`YNbI^7)9>X65aC|{2!g>}WJGvGL|1@h2be19bN(8VXPcPK zOy28E2?3#5Mn^|ZgIFIl>h!Up^>uYUJ3Bk6fCG8`&`;!nM8-#FmK|L*u*KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000ecNklUZ;Lse3y z42h2v8mT0rqNP@8@0d$;8=N#1I7TOfx$(7 zXD6@)Xa$;q*W|GY*p~&P)uenHI373+m<*gCgjWKsz)L_Aum*V33ZQa04mcNR1STb< zP3wUtffc~3RsiK0&(8;D0;fjseS3k=9JldW84mkL@ZHY?4+G26;5Q^)0LK8A0T%%W zh4|d3z}vuPpha%kABuCgOPswfasCizaF95M2a2;eLWY#Fz>z>bFeF49+JT3F`+(Q2 z0K#DsFc+8+@cr9?b-)Y21~K@yh}%>rf@}az22KJF4*1!nz#TGFSOMfgJunZL72v&_ zfv08cT_gYMN+Kr?Xd^IH?h=Kt1h`S|PF4U83<9nNu6E4L{ci2coHU* zwOHlHMy1qjrBvS-+ga?Rue5z*fj%x4Tg1}NxQsJZDbgej@zsoOvBfop z*9M3Ur=@KOm?91DV;Jve0)Mi+8*w3eC(-Q}^vH7asx%0o0rDfiZG2XJPUkN07n!`0{wCnws`;cjh76u@BgHtbl#KYy|% zzbFwj8SVrv=xyoFq&b0)7|2)X?NUpAaWjNQv7viS<&mV#0eW-6t+ zeD>Av&aep98v47SQt<$zfOif4!?z9(Zt!8qVwu4_HO;bB(^ zi*W)M8a}r$2J#6o&QFsCl`myX0L_MJVIQBVVx&&s(Rfe7?m&k^?~TC_^U(42g|Hns z3%yT~74%1k$h%`L#*3p^mOFrH_32kJksk&JqmN~sogo42725~lC=vmTH7rZEpbu=M z0sYY1tUt++2=2!lH+TWwJ()-ZaE0O84JkC*ABq_(6O0=Ia#fNKkVEIm+0SvU4V``^ z&QvKueE<`dqN9`2j#_jYn^BHypP=(2bwqUs_<@UjfICTdo}3xzpv(PJE`WW)g%kS; zXORs7vt7@-(BaH!$H_7?I6US3xCie^WB0l#wxXG&+*$ z3Nn%;(FY5GyHe^5u5evDhWtq?3JTyGhHERyK7I;3PX2T?NyN&490nG^nXc#UGFB!9 zHD&zn>F8{r!&4-HwZc(84g(9Imj6fD^kN-4QB$?xtLVJI<5SY!PlY2VP7p3b3t$2| z%eE71s6LDUUID(DlJ>eeI#jp}EdWz8hxJq^2B3pWE=xgM*9YXXpa70{J@1m+VKw7k zbiiqnu?g?0;PGJva3DGY)rmLJ!5`I*>(DX!NyQGlK|A%rXrdSEcc&d7JA`j zNJ87U#oz~w3?qOsz!9!%&6f8s;B;UkU)+XG3fx>Bflo0FKLH$$&fn|AX3Mz;N6MIc zV#3%Hr+raoa z@m4hJG6-N`7y-DqL3>F4;g2NYD48b4g}r!>&Gic-0E^NwFb$3L6F|4&ef=$`fw2#7 zcvdCR-@t8m7y;}y+&%_bUIXI^bb9=_FvxJr-yKE(9R>m9EU$sk4qO@Yv^i%GKt~t> z1fEA)d;$MOpD*}#%+UXSw#^O5kS+Y?qs> zcZAcx0{Fl%j~#9K{u~PUK6>*fiEwkZRX7bb3vfF#<4M;ciE`m%bmfj^32onSyyqC+ z4m%3~9SiSXT$oVh8X7%L^yKdCGs5SFSH%J z+V%V(;FKyN|1$JJkhI_wys>|)^L?O@0G=^in_dOP??&etzCUH{o*t0HzyfGSCs1BN2Pv-MU?1^&H(C;`7f#bU8w@dp>xu?k0LE0Jz-R7 z!D7QqW*hnxP|^9DO3e_`fw?fzEYe$hCyBV)XBv9}B_{8RB1Oz|z^li?$S6bfp16g15pZKFgga!y*Aw`9eLUE z8445Sk2BbHy#-wjtTT!qi84!AC;IS2yFnPMr9&3M$zN?Czg=wH8PyL(ohGggI1AWg zm@PDs-C)uv(K}?ib29A_+qOmdW97&bxB)mDZ=%A%z$)NE%dsym6dSq!`8{IO4N?DK zxe`UL@)1FeEL_+rqIs&f)Zp;-lE11PKN_VIXu=e9&b$GJ*A@Xk#wUSc6`>z`!^#xa zy{DYym-7zbyc3*)IoAGS;KeF;4kwBYOyutn+pQ{R11ga#_+{X0n9rymhd#YJw+hLh zD}6uC@Y)+E&r*s7%>t%+evbH2)t zo0L+y3~uG-q;L5!ztZiB%NSFYQmuIm(W;c1lOgiw1nBS7INBDcQHCm|78m$OY*b3k zR!a3vA?bak?Hdd9ak1DUmUfnV8W;rU0JoAq5oI$vS;IrX`w1W84*@Ph$LtsC(^cqG z$*~~`0swum@mg67>6hnypNf;T6kW}`Cn2QwK?k7D0L~+S9;I${Ww8Y$pbk?~1kj6m zU>?C@_%{Pj15W~LsC-qk96BRcBQTXq@dhxS_I%;K0a5*-lU|iT`#`K3UuX=)F6!laALeTVw2Go zJH}M#cUxs0zeya>H`8jsvY$8wkRV#qx z_zfe0!_fKj>cqJ_K%BjSjx~pF^kLEtbo!VbGK<&>yoWBfurrHB%CZ0~0>IV|wgRxQ h0Mw?C&ZhtX002ovPDHLkV1g&$no0lw diff --git a/nise-frontend/src/assets/replay-viewer/sliderball.png b/nise-frontend/src/assets/replay-viewer/sliderball.png deleted file mode 100644 index 316d52c685d1169bf9bb991d6b2d8bd4b3797c61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10899 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 Ml9pnX{L}FN0ew%+2><{9 diff --git a/nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts b/nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts deleted file mode 100644 index f93d6c1..0000000 --- a/nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {KeyPress, ReplayEvent} from "./replay-viewer.component"; - -export function getEvents(replayString: string): ReplayEvent[] { - const trimmedReplayDataStr = replayString.endsWith(',') ? replayString.slice(0, -1) : replayString; - return processEvents(trimmedReplayDataStr); -} - -function processEvents(replayDataStr: string): ReplayEvent[] { - const eventStrings = replayDataStr.split(","); - const playData: ReplayEvent[] = []; - eventStrings.forEach((eventStr, index) => { - const event = createReplayEvent(eventStr.split('|'), index, eventStrings.length); - if (event) playData.push(event); - }); - return playData; -} - -function createReplayEvent(eventParts: string[], index: number, totalEvents: number): ReplayEvent | null { - const timeDelta = parseInt(eventParts[0], 10); - const x = parseFloat(eventParts[1]); - const y = parseFloat(eventParts[2]); - const rawKey = parseInt(eventParts[3], 10); - - if (timeDelta == -12345 && index == totalEvents - 1) { - return null; - } - if (index < 2 && x == 256.0 && y == -500.0) { - return null; - } - - let keys: KeyPress[] = []; - Object.keys(KeyPress).forEach(key => { - const keyPress = KeyPress[key as keyof typeof KeyPress]; - if ((rawKey & keyPress) === keyPress) { - keys.push(keyPress); - } - }); - - return { - timeDelta, - x, - y, - keys - }; -} diff --git a/nise-frontend/src/corelib/components/replay-viewer/process-replay.ts b/nise-frontend/src/corelib/components/replay-viewer/process-replay.ts deleted file mode 100644 index cbd9031..0000000 --- a/nise-frontend/src/corelib/components/replay-viewer/process-replay.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {KeyPress, ReplayEvent} from "./replay-viewer.component"; - -export class ReplayEventProcessed { - x: number; - y: number; - t: number; - keys: KeyPress[]; - - constructor(x: number, y: number, t: number, keys: KeyPress[]) { - this.x = x; - this.y = y; - this.t = t; - this.keys = keys; - } -} - -export function processReplay(events: ReplayEvent[], mods: string[], audioLeadIn: number): ReplayEventProcessed[] { - console.log(events); - - if (events.length === 0) throw new Error("This replay's replay data was empty. It indicates a misbehaved replay."); - - if (events[0].timeDelta === 0 && events.length > 1) events.shift(); - - const pEvents: ReplayEventProcessed[] = []; - - let cumulativeTimeDelta = events[0].timeDelta + audioLeadIn; - let highestTimeDelta = Number.NEGATIVE_INFINITY; - let lastPositiveFrame: ReplayEvent | null = null; - - let wasInNegativeSection = false; - const lastPositiveFrameData: [number, [number, number]][] = []; - - const timeModifier = mods.includes("DT") ? 2 / 3 : mods.includes("HT") ? 4 / 3 : 1; - - events.slice(1).forEach((currentFrame, index) => { - currentFrame.timeDelta *= timeModifier; - if(mods.includes("HR")) { - currentFrame.y = 384 - currentFrame.y; - } - - const previousCumulativeTime = cumulativeTimeDelta; - cumulativeTimeDelta += currentFrame.timeDelta; - - highestTimeDelta = Math.max(highestTimeDelta, cumulativeTimeDelta); - - const isInNegativeSection = cumulativeTimeDelta < highestTimeDelta; - if (isInNegativeSection) { - if (!wasInNegativeSection) { - lastPositiveFrame = index > 0 ? events[index] : null; - } - } else { - if (wasInNegativeSection && lastPositiveFrame) { - const lastPositiveTime = lastPositiveFrameData.length > 0 ? lastPositiveFrameData[lastPositiveFrameData.length - 1][0] : previousCumulativeTime; - const ratio = (lastPositiveTime - previousCumulativeTime) / (cumulativeTimeDelta - previousCumulativeTime); - - const interpolatedX = lastPositiveFrame.x + ratio * (currentFrame.x - lastPositiveFrame.x); - const interpolatedY = lastPositiveFrame.y + ratio * (currentFrame.y - lastPositiveFrame.y); - - pEvents.push(new ReplayEventProcessed(interpolatedX, interpolatedY, lastPositiveTime, lastPositiveFrame.keys)); - } - wasInNegativeSection = false; - } - - wasInNegativeSection = isInNegativeSection; - - if (!isInNegativeSection) { - pEvents.push(new ReplayEventProcessed(currentFrame.x, currentFrame.y, cumulativeTimeDelta, currentFrame.keys)); - } - - if (!isInNegativeSection) { - lastPositiveFrameData.push([cumulativeTimeDelta, [currentFrame.x, currentFrame.y]]); - } - }); - - // Ensuring uniqueness based on time to avoid duplicates - return pEvents.filter((event, index, self) => - index === self.findIndex((t) => ( - t.t === event.t - )) - ); -} diff --git a/nise-frontend/src/corelib/components/replay-viewer/replay-service.ts b/nise-frontend/src/corelib/components/replay-viewer/replay-service.ts deleted file mode 100644 index 341dda5..0000000 --- a/nise-frontend/src/corelib/components/replay-viewer/replay-service.ts +++ /dev/null @@ -1,497 +0,0 @@ -import { ElementRef } from '@angular/core'; -import {processReplay, ReplayEventProcessed} from "./process-replay"; -import {ReplayViewerData, Slider} from "../../../app/replays"; -import {getEvents} from "./decode-replay"; - -export class ReplayService { - - private replayViewerData: ReplayViewerData; - private readonly replayEvents: ReplayEventProcessed[] = []; - - currentTime = 0; - speedFactor: number = 1; - - private totalDuration = 0; - private lastRenderTime = 0; - - private isPlaying = false; - private requestId: number | null = null; - - private replayCanvas: ElementRef | null = null; - private ctx: CanvasRenderingContext2D | null = null; - - private hitCircleImage = new Image(); - private hitCircleOverlay = new Image(); - private approachCircleImage = new Image(); - private cursorImage = new Image(); - private sliderBall = new Image(); - private reverseArrow = new Image(); - - constructor(replayViewerData: ReplayViewerData) { - this.hitCircleImage.src = 'assets/replay-viewer/hitcircle.png'; - this.hitCircleOverlay.src = 'assets/replay-viewer/hitcircleoverlay.png'; - this.approachCircleImage.src = 'assets/replay-viewer/approachcircle.png'; - this.cursorImage.src = 'assets/replay-viewer/cursor.png'; - this.sliderBall.src = 'assets/replay-viewer/sliderball.png'; - this.reverseArrow.src = 'assets/replay-viewer/reversearrow.png'; - - this.replayViewerData = replayViewerData; - this.replayEvents = processReplay(getEvents(replayViewerData.replay), replayViewerData.mods, replayViewerData.beatmap.audio_lead_in); - this.calculateTotalDuration(); - } - - setCanvasElement(canvas: ElementRef) { - this.replayCanvas = canvas; - } - - setCanvasContext(ctx: CanvasRenderingContext2D) { - this.ctx = ctx; - } - - private calculateTotalDuration() { - if (this.replayEvents.length === 0) { - this.totalDuration = 0; - return; - } - const lastEvent = this.replayEvents[this.replayEvents.length - 1]; - this.totalDuration = lastEvent.t; - } - - start() { - this.isPlaying = true; - if(this.currentTime >= this.totalDuration) { - this.currentTime = 0; - } - this.animate(); - } - - pause() { - this.isPlaying = false; - if (this.requestId) { - cancelAnimationFrame(this.requestId); - this.requestId = null; - } - } - - seek(time: number) { - this.currentTime = Math.min(Math.max(time, 0), this.totalDuration); - this.lastRenderTime = 0; - this.isPlaying = true; - this.animate(); - this.isPlaying = false; - } - - private animate(currentTimestamp: number = 0) { - if (!this.isPlaying) return; - - if (!this.lastRenderTime) { - this.lastRenderTime = currentTimestamp; - } - - if(!this.ctx || !this.replayCanvas) { - console.error('Canvas context not initialized'); - return; - } - - const elapsedTime = currentTimestamp - this.lastRenderTime; - - // Check if enough time has passed for the next frame (approximately 16.67ms for 60 FPS) - if (elapsedTime < 16.67) { - // Request the next animation frame and return early - this.requestId = requestAnimationFrame(this.animate.bind(this)); - return; - } - - // Assuming elapsedTime is sufficient for 1 frame, update currentTime for real-time playback - this.currentTime += elapsedTime * this.speedFactor; - - if (this.currentTime > this.totalDuration) { - this.currentTime = this.totalDuration; - this.pause(); - return; - } - - this.ctx.clearRect(0, 0, this.replayCanvas.nativeElement.width, this.replayCanvas.nativeElement.height); - this.drawHitCircles(); - this.drawSliders(); - this.drawCursor(); - - this.lastRenderTime = currentTimestamp; - - // Request the next frame - this.requestId = requestAnimationFrame(this.animate.bind(this)); - } - - public getCurrentReplayEvent(): ReplayEventProcessed | null { - let currentEvent: ReplayEventProcessed | null = null; - for (const event of this.replayEvents) { - if (event.t <= this.currentTime) { - currentEvent = event; - } else { - break; // Exit the loop once an event exceeds the current time - } - } - return currentEvent; - } - - private drawCursor() { - const currentEvent = this.getCurrentReplayEvent(); - if (currentEvent) { - if (!this.ctx || !this.replayCanvas) { - console.error('Canvas context not initialized'); - return; - } - - this.ctx.drawImage(this.cursorImage, currentEvent.x - 32, currentEvent.y - 32, 64, 64); - } - } - - private drawHitCircles() { - if (!this.ctx || !this.replayCanvas) { - console.error('Canvas context not initialized'); - return; - } - - const visibleHitCircles = this.replayViewerData!.beatmap.circles.filter(obj => - this.currentTime >= obj.time - 200 && - this.currentTime <= obj.time - ); - - visibleHitCircles.forEach(hitCircle => { - const opacity = this.calculateOpacity(hitCircle.time); - this.drawHitCircle(hitCircle.x, hitCircle.y, opacity, hitCircle.current_combo, hitCircle.combo_color); - this.drawApproachCircle(hitCircle.x, hitCircle.y, 1, hitCircle.time) - }); - } - - private drawApproachCircle(x: number, y: number, opacity: number, hitTime: number) { - let {fadeIn, totalDisplayTime} = this.calculatePreempt(); - let baseSize = 54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size; - - // Calculate scale using the provided formula - let scale = Math.max(1, ((hitTime - this.currentTime) / totalDisplayTime) * 3 + 1); - - // Adjust baseSize according to the scale - baseSize *= scale; - - this.ctx!.drawImage(this.approachCircleImage, x - baseSize, y - baseSize, baseSize * 2, baseSize * 2); - } - - private calculateOpacity(circleTime: number): number { - const timeDifference = circleTime - this.currentTime; - if (timeDifference < 0) { - return 0; // Circle time has passed, so it should be fully transparent - } - - let {fadeIn, totalDisplayTime} = this.calculatePreempt(); - - // Adjust the opacity calculation based on fadeIn and totalDisplayTime - if (timeDifference > totalDisplayTime) { - return 0; // Circle is not visible yet - } else if (timeDifference > fadeIn) { - return 1; // Circle is fully visible (opaque) - } else { - return (fadeIn - timeDifference) / fadeIn; - } - } - - private calculatePreempt() { - const AR = this.replayViewerData.beatmap.difficulty.approach_rate; - let fadeIn = 0; - - if (AR === 5) { - fadeIn = 800; - } else if (AR > 5) { - fadeIn = 800 - 500 * (AR - 5) / 5; - } else if (AR < 5) { - fadeIn = 800 + 400 * (5 - AR) / 5; - } - - let stayVisibleTime: number; - if (AR > 5) { - stayVisibleTime = 1200 + 600 * (5 - AR) / 5; - } else if (AR < 5) { - stayVisibleTime = 1200 - 750 * (AR - 5) / 5; - } else { - stayVisibleTime = 1200; - } - - let totalDisplayTime = fadeIn + stayVisibleTime; - return {fadeIn, totalDisplayTime}; - } - - private drawHitCircle(x: number, y: number, opacity: number, combo: number, color: string) { - this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect - let radius = 54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size; - - this.ctx!.drawImage(this.hitCircleImage, x - radius, y - radius, radius * 2, radius * 2); - this.ctx!.drawImage(this.hitCircleOverlay, x - radius, y - radius, radius * 2, radius * 2); - - // Draw combo - this.ctx!.font = "32px monospace"; - const measure = this.ctx!.measureText(combo.toString()); - this.ctx!.fillText(combo.toString(), x - measure.width / 2, y + 10); - } - - private drawSliderHitCircle(x: number, y: number, opacity: number, combo: number) { - this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect - let radius = 54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size; - - this.ctx!.drawImage(this.hitCircleImage, x - radius, y - radius, radius * 2, radius * 2); - this.ctx!.drawImage(this.hitCircleOverlay, x - radius, y - radius, radius * 2, radius * 2); - - // Draw combo - this.ctx!.font = "32px monospace"; - const measure = this.ctx!.measureText(combo.toString()); - this.ctx!.fillText(combo.toString(), x - measure.width / 2, y + 10); - } - - private drawSliders() { - if (!this.ctx || !this.replayCanvas) { - console.error('Canvas context not initialized'); - return; - } - - const visibleSliders = this.replayViewerData.beatmap.sliders.filter(obj => { - return this.currentTime >= obj.time - 200 && // Start showing 200ms before - this.currentTime <= obj.end_time; // Hide after its time - }); - - visibleSliders.forEach(slider => { - const opacity = this.calculateOpacity(slider.time); - this.drawSliderBody(slider); - this.drawSliderHitCircle(slider.x, slider.y, opacity, slider.current_combo); - this.drawApproachCircle(slider.x, slider.y, 1, slider.time) - }); - } - - drawSliderBody(slider: Slider): void { - let playfieldScale = 1; - let mapCsr = (54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size) * 2; - let SLIDERSZ = -7 + mapCsr; - let SLIDERBORDERSIZE = 5; - - let duration = slider.end_time - slider.time; - - let sliderBallsizeX = mapCsr * (this.sliderBall.width / this.sliderBall.width); - let sliderBallsizeY = mapCsr * (this.sliderBall.height / this.sliderBall.width); - - // TODO - var slidesDone; - if (this.currentTime - slider.time < 0){ - slidesDone = 0; - } else { - slidesDone = Math.floor((this.currentTime - slider.time) / duration); - } - - if(slider.curve.type == "Linear") { - this.ctx!.beginPath(); - var start = [slider.x, slider.y]; - var end = [slider.curve.points.slice(- 1)[0].x, slider.curve.points.slice(- 1)[0].y]; - var clength = Math.pow(Math.pow((end[0] - start[0]), 2) + Math.pow((end[1] - start[1]), 2), 0.5); - if (clength < length * playfieldScale){ - this.ctx!.lineTo(((end[0] - start[0]) / clength) * length * playfieldScale + start[0], ((end[1] - start[1]) / clength) * length * playfieldScale + start[1]); - clength = length * playfieldScale; - } - this.ctx!.moveTo(start[0], start[1]); - this.ctx!.lineTo(end[0], end[1]); - this.ctx!.lineCap = 'round'; - this.ctx!.lineWidth = SLIDERSZ - SLIDERBORDERSIZE; - this.ctx!.strokeStyle = 'rgb(3,3,12,0.5)';//} - this.ctx!.stroke(); - this.ctx!.closePath(); - - - this.ctx!.save(); - this.ctx!.translate(start[0], start[1]); - if (end[0] > start[0]) { //I don't have enough braincells to figure out why this is a thing - this.ctx!.rotate(Math.atan((end[1] - start[1]) / (end[0] - start[0])) - Math.PI / 2); - } else { - this.ctx!.rotate(Math.atan((end[1] - start[1]) / (end[0] - start[0])) + Math.PI / 2); - } - this.ctx!.beginPath(); - this.ctx!.moveTo(- SLIDERSZ / 2, 0); - this.ctx!.lineTo(- SLIDERSZ / 2, clength); - this.ctx!.arc (0, clength, SLIDERSZ / 2, Math.PI, 0, true); - //this.ctx!.moveTo( SLIDERSZ / 2, length*playfieldScale); - this.ctx!.lineTo(SLIDERSZ / 2, 0); - this.ctx!.arc (0, 0, SLIDERSZ / 2, 0, Math.PI, true); - //this.ctx!.moveTo(-SLIDERSZ / 2, 0); - this.ctx!.lineCap = 'butt'; - this.ctx!.lineWidth = SLIDERBORDERSIZE; - this.ctx!.strokeStyle = 'rgb(190,190,190)'; - //this.ctx!.strokeRect(-SLIDERSZ / 2,0,SLIDERSZ,length*playfieldScale); - this.ctx!.stroke(); - this.ctx!.closePath(); - - - - //console.log(slidesDone); - if (slidesDone % 2 === 0){ - this.ctx!.translate(0, clength); - this.ctx!.rotate(- Math.PI / 2); - } else { - this.ctx!.rotate(Math.PI / 2); - } - - //Draw sliderend (if any) - //1 slide is duration long. current-timing is elapsed time. (current-timing)/duration is amount of slides gone thru. - //therefore, amount of slides left is slides-(current-timing)/duration. But current-timing must be gt 0 - if (this.currentTime < slider.time){ - if (slider.repeat > 1) { - this.ctx!.drawImage(this.reverseArrow, - mapCsr / 2, - mapCsr / 2, mapCsr, mapCsr); - } - } else { - if (slider.repeat - slidesDone > 1) { - this.ctx!.drawImage(this.reverseArrow, - mapCsr / 2, - mapCsr / 2, mapCsr, mapCsr); - } - if ((slider.repeat - slidesDone > 2)) { //TODO, I can't test this right now, but im pretty sure this will render it when it isnt supposed to be - this.ctx!.drawImage(this.reverseArrow, - mapCsr / 2, - mapCsr / 2, mapCsr, mapCsr); //REMEMEBER TO EDIT THIS FOR ALL SLIDERS !! - } - } - - this.ctx!.restore(); - - //Draw sliderball along the path - if (this.currentTime >= slider.time && this.currentTime < slider.time + duration * 1) { - var sliderBall = (this.currentTime - (slider.time + duration * slidesDone)) * (clength / duration); - - var sliderBallX; var sliderBallY; - if (slidesDone % 2 === 0){ - sliderBallX = ((end[0] - start[0]) / clength) * sliderBall + start[0]; - sliderBallY = ((end[1] - start[1]) / clength) * sliderBall + start[1]; - } else { - sliderBallX = ((start[0] - end[0]) / clength) * sliderBall + end[0]; - sliderBallY = ((start[1] - end[1]) / clength) * sliderBall + end[1]; - } - - this.ctx!.drawImage(this.sliderBall, sliderBallX - sliderBallsizeX / 2, sliderBallY - sliderBallsizeX / 2, sliderBallsizeX, sliderBallsizeX); - } - } - - if (slider.curve.type == "Perfect"){ - var start = [slider.x, slider.y]; - var mid = [slider.curve.points[1].x, slider.curve.points[1].y]; - var end = [slider.curve.points[2].x, slider.curve.points[2].y]; - - var arcMidpoint = []; - start[0] -= mid[0]; //Translate points so mid is at origin - start[1] -= mid[1]; - end[0] -= mid[0]; - end[1] -= mid[1]; - var D = 2*(start[0]*end[1]-end[0]*start[1]); - var z1 = start[0]*start[0] + start[1]*start[1]; - var z2 = end[0]*end[0]+end[1]*end[1]; - arcMidpoint[0] = (z1 * end[1] - z2 * start[1]) / D + mid[0]; - arcMidpoint[1] = (start[0] * z2 - end[0] * z1) / D + mid[1]; - - start[0] += mid[0]; - start[1] += mid[1]; - end[0] += mid[0]; - end[1] += mid[1]; - - - var arcRadius = Math.pow(Math.pow(mid[0]-arcMidpoint[0],2)+Math.pow(mid[1]-arcMidpoint[1],2),0.5); - //var arcStartAngle = -arctan((arcMidpoint[1]-start[1])/(arcMidpoint[0]-start[0])); - //var arcEndAngle = -arctan((end[1]-arcMidpoint[1])/(end[0]-arcMidpoint[0])); - var arcStartAngle = Math.atan2(start[1] - arcMidpoint[1], start[0] - arcMidpoint[0]); - var arcEndAngle = Math.atan2(end[1] - arcMidpoint[1], end[0] - arcMidpoint[0]); - var isClockwise = ((end[0] - start[0]) * (mid[1] - start[1]) - (end[1] - start[1]) * (mid[0] - start[0])) >= 0 - var clength = Math.abs(arcEndAngle - arcStartAngle) * arcRadius; - if (length > clength) { - arcEndAngle += (length * playfieldScale - clength) / arcRadius; - end = [arcMidpoint[0] + arcRadius * Math.cos(arcEndAngle),arcMidpoint[1] + arcRadius * Math.sin(arcEndAngle)]; - } - //console.log(arcMidpoint[0], arcMidpoint[1], arcRadius, arcStartAngle, arcEndAngle) - - this.ctx!.beginPath(); - this.ctx!.arc(arcMidpoint[0], arcMidpoint[1], arcRadius, arcStartAngle, arcEndAngle, isClockwise); - this.ctx!.lineCap = 'round'; - //this.ctx!.lineWidth = 10; - this.ctx!.lineWidth = SLIDERSZ - SLIDERBORDERSIZE; - // this.ctx!.strokeStyle = 'rgb(200,100,100)'; - this.ctx!.strokeStyle = 'rgb(3,3,12,0.5)';//} - this.ctx!.stroke(); - this.ctx!.closePath(); - - //Now draw the slider border //This might have just been the most satisfying thing I've made it a while - this.ctx!.beginPath(); //its so nice when the math just works out how you calculated it to - - let effectiveRadius = arcRadius - SLIDERSZ/2; - if (effectiveRadius < 0) { - // Option 1: Adjust SLIDERSZ to ensure effectiveRadius is positive - SLIDERSZ = 2 * arcRadius; // This is just an example adjustment - effectiveRadius = arcRadius - SLIDERSZ/2; // Recalculate effectiveRadius - } - - this.ctx!.arc(arcMidpoint[0], arcMidpoint[1], arcRadius+SLIDERSZ/2, arcStartAngle, arcEndAngle, isClockwise); - this.ctx!.arc(end[0], end[1], SLIDERSZ/2, arcEndAngle, arcEndAngle+Math.PI,isClockwise); - this.ctx!.arc(arcMidpoint[0], arcMidpoint[1], effectiveRadius, arcEndAngle, arcStartAngle, !isClockwise); - this.ctx!.arc(start[0], start[1], SLIDERSZ/2, arcStartAngle+Math.PI, arcStartAngle,isClockwise); - this.ctx!.lineCap = 'butt'; - this.ctx!.lineWidth = SLIDERBORDERSIZE; - this.ctx!.strokeStyle = 'rgb(190,190,190)'; - // this.ctx!.strokeStyle = 'rgb(200,100,100)'; - this.ctx!.stroke(); - this.ctx!.closePath(); - - //TODO PRIORITY 1 !!!! !!! !!! !!! !!! fix the sliderend, it looks slanted I swear - this.ctx!.save(); - // if (slidesDone % 2 === 0){ - // this.ctx!.translate(end[0],end[1]); - // //this.ctx!.rotate(arcEndAngle + pi / 2 + (isClockwise ? 0 : pi)); //This was my first theory on how to rotate the slider - // this.ctx!.rotate(arcEndAngle + Math.PI / 2 + (isClockwise ? 0.1 : Math.PI-0.1)); - // } else { - // this.ctx!.translate(start[0],start[1]); //TODO cannot test this - // this.ctx!.rotate(arcStartAngle - Math.PI / 2 + (isClockwise ? Math.PI-0.1 : 0.1)); - // } - - // if (current < timing){ - // if (slides > 1) { - // this.ctx!.drawImage(assets['reverseArrow'], - mapCSr / 2, - mapCSr / 2, mapCSr, mapCSr); - // } - // } else { - // if (slides - slidesDone > 1) { - // this.ctx!.drawImage(assets['reverseArrow'], - mapCSr / 2, - mapCSr / 2, mapCSr, mapCSr); - // } - // //if ((slides - slidesDone > 2)) { //TODO, I can't test this right now, but im pretty sure this will render it when it isnt supposed to be - // // this.ctx!.drawImage(assets['reverseArrow'], end[0] - mapCSr / 2, end[1] - mapCSr / 2, mapCSr, mapCSr); - // //}//TODO THIS WONT WORK AAAAA - // } - this.ctx!.restore(); - - // //Draw sliderball along the path - let timing = slider.time - - if (this.currentTime >= timing && this.currentTime < timing + duration * 1) { - var angleDt = Math.abs(arcStartAngle-arcEndAngle); - //This took way too long and I don't even know why it works or if it there is possible bugged behavior - //If anyone can explain what is happening here or the best practice I would be super thankful - if ((isClockwise && (arcStartAnglearcEndAngle))) {angleDt = 2*Math.PI - angleDt;} - var sliderBall = (isClockwise?-1:1)*(this.currentTime - (timing + duration * slidesDone)) * (angleDt / duration); - - - var sliderBallX; var sliderBallY; - if (slidesDone % 2 === 0){ - sliderBallX = arcMidpoint[0] + arcRadius * Math.cos(arcStartAngle + sliderBall); - sliderBallY = arcMidpoint[1] + arcRadius * Math.sin(arcStartAngle + sliderBall); - } else { - sliderBallX = arcMidpoint[0] + arcRadius * Math.cos(arcEndAngle - sliderBall); - sliderBallY = arcMidpoint[1] + arcRadius * Math.sin(arcEndAngle - sliderBall); - } - - this.ctx!.drawImage(this.sliderBall, sliderBallX - sliderBallsizeX / 2, sliderBallY - sliderBallsizeX / 2, sliderBallsizeX, sliderBallsizeX); - } - - } - } - - getTotalDuration() { - return this.totalDuration; - } - - getIsPlaying() { - return this.isPlaying; - } - -} diff --git a/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.css b/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.css deleted file mode 100644 index 3ad814f..0000000 --- a/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.css +++ /dev/null @@ -1,39 +0,0 @@ -.row { - display: flex; - flex-direction: row; - flex-wrap: wrap; - width: 100%; -} - -.column { - display: flex; - flex-direction: column; - flex-basis: 100%; - flex: 1; -} - -ul { - list-style: none; -} - -/* Flex container */ -.flex-container { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 20px; /* Adjust the gap between items as needed */ -} - -/* Flex items - default to full width to stack on smaller screens */ -.flex-container > div { - flex: 0 0 100%; - box-sizing: border-box; /* To include padding and border in the element's total width and height */ -} - -/* Responsive columns */ -@media (min-width: 768px) { /* Adjust the breakpoint as needed */ - .flex-container > div { - flex: 0 0 40%; - max-width: 50%; - } -} diff --git a/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.html b/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.html deleted file mode 100644 index 0c77a50..0000000 --- a/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.html +++ /dev/null @@ -1,43 +0,0 @@ -
- -
- - -
- -
- -
-
- -
- -
- -
- -
-
- -
- -
- -
- -
- -
- -
- - -
- -
-
-
- - - - diff --git a/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.ts b/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.ts deleted file mode 100644 index 0f50505..0000000 --- a/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; -import {DecimalPipe, JsonPipe, NgForOf, NgIf} from "@angular/common"; -import {ReplayService} from "./replay-service"; -import {FormsModule} from "@angular/forms"; -import {ReplayViewerData} from "../../../app/replays"; - -export enum KeyPress { - M1 = 1, - M2 = 2, - K1 = 5, - K2 = 10, - Smoke = 16, -} - -export interface ReplayEvent { - /** - * Time in milliseconds since the previous action - */ - timeDelta: number; - - /** - * x-coordinate of the cursor from 0 - 512 - */ - x: number; - - /** - * y-coordinate of the cursor from 0 - 384 - */ - y: number; - - /** - * Keys being pressed. - */ - keys: KeyPress[]; - -} - -@Component({ - selector: 'app-replay-viewer', - standalone: true, - imports: [ - JsonPipe, - FormsModule, - DecimalPipe, - NgForOf, - NgIf - ], - templateUrl: './replay-viewer.component.html', - styleUrl: './replay-viewer.component.css' -}) -export class ReplayViewerComponent implements AfterViewInit { - - @ViewChild('replayCanvas') replayCanvas!: ElementRef; - private ctx!: CanvasRenderingContext2D; - - @Input() replayViewerData!: ReplayViewerData; - - public replayService!: ReplayService | null; - - // TODO: Calculate AudioLeadIn - // TODO: Hard-Rock, DT, Easy - - // TODO: Cursor trail and where keys are pressed - // TODO: Button for -100 ms, +100 ms, etc (precise seeking) (or keyboard shortcuts) - - // TODO: UR bar - // Todo: Customizable speed - // TODO: Customizable zoom - // TODO: Fullscreen mode - - // TODO: Hit/Miss, Combo, Accuracy - - // TODO: Compare two replays for similarity (different cursor color) - - constructor() { - - } - - ngAfterViewInit() { - this.replayService = new ReplayService(this.replayViewerData); - this.ctx = this.replayCanvas.nativeElement.getContext('2d')!; - - this.replayService.setCanvasElement(this.replayCanvas); - this.replayService.setCanvasContext(this.ctx); - - this.replayService.start(); // Start the animation loop - } - - togglePlayPause() { - if(!this.replayService) return; - - if (this.replayService.getIsPlaying()) { - this.replayService.pause(); - } else { - this.replayService.start(); - } - } - - seek(time: number) { - if(!this.replayService) return; - - this.replayService.seek(time); - if (!this.replayService.getIsPlaying()) { - // Redraw the canvas for the new current time without resuming playback - } - } - -} diff --git a/nise-frontend/src/corelib/components/replay-viewer/sample-replay.ts b/nise-frontend/src/corelib/components/replay-viewer/sample-replay.ts deleted file mode 100644 index f69ae9c..0000000 --- a/nise-frontend/src/corelib/components/replay-viewer/sample-replay.ts +++ /dev/null @@ -1,224 +0,0 @@ -export let replayData = "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" -export let beatmapData = "osu file format v14\n" + - "\n" + - "[General]\n" + - "AudioFilename: audio.mp3\n" + - "AudioLeadIn: 0\n" + - "PreviewTime: 932\n" + - "Countdown: 0\n" + - "SampleSet: Soft\n" + - "StackLeniency: 0.5\n" + - "Mode: 0\n" + - "LetterboxInBreaks: 0\n" + - "WidescreenStoryboard: 1\n" + - "\n" + - "[Editor]\n" + - "Bookmarks: 1327,11432,21537,31642\n" + - "DistanceSpacing: 0.5\n" + - "BeatDivisor: 4\n" + - "GridSize: 32\n" + - "TimelineZoom: 3.15\n" + - "\n" + - "[Metadata]\n" + - "Title:PADORU / PADORU\n" + - "TitleUnicode:PADORU / PADORU\n" + - "Artist:Turbo\n" + - "ArtistUnicode:Turbo\n" + - "Creator:DeRandom Otaku\n" + - "Version:Rolniczy's Hi-Speed Expert\n" + - "Source:Fate/EXTRA\n" + - "Tags:TurboAutism eurobeats nero claudius ren kowari -_frontier_- anzeigeistraus iljaaz rolniczy fuju smokelind deppyforce xenon- xehn affirmation neoskylove dorsalplum -_light_- xen rolni contagious Serizawa Haruki Jack J_a_c_k electronic japanese jingle bells 丹下桜 Sakura Tange Saber\n" + - "BeatmapID:2223135\n" + - "BeatmapSetID:1061287\n" + - "\n" + - "[Difficulty]\n" + - "HPDrainRate:6\n" + - "CircleSize:3.6\n" + - "OverallDifficulty:8.6\n" + - "ApproachRate:9.5\n" + - "SliderMultiplier:2.1\n" + - "SliderTickRate:1\n" + - "\n" + - "[Events]\n" + - "//Background and Video events\n" + - "0,0,\"bg.jpg\",0,0\n" + - "//Break Periods\n" + - "//Storyboard Layer 0 (Background)\n" + - "//Storyboard Layer 1 (Fail)\n" + - "//Storyboard Layer 2 (Pass)\n" + - "//Storyboard Layer 3 (Foreground)\n" + - "//Storyboard Layer 4 (Overlay)\n" + - "//Storyboard Sound Samples\n" + - "\n" + - "[TimingPoints]\n" + - "64,315.789473684211,4,2,0,60,1,0\n" + - "64,-100,4,2,0,60,0,0\n" + - "1011,-100,4,2,1,45,0,0\n" + - "1327,-100,4,2,1,60,0,0\n" + - "5748,-200,4,2,1,60,0,0\n" + - "8590,-125,4,2,1,65,0,0\n" + - "11432,-66.6666666666667,4,2,1,80,0,0\n" + - "12853,-62.5,4,2,1,80,0,0\n" + - "13327,-71.4285714285714,4,2,1,80,0,0\n" + - "13958,-66.6666666666667,4,2,1,80,0,0\n" + - "14274,-62.5,4,2,1,80,0,0\n" + - "14590,-66.6666666666667,4,2,1,80,0,0\n" + - "15379,-58.8235294117647,4,2,1,80,0,0\n" + - "15853,-66.6666666666667,4,2,1,80,0,0\n" + - "16485,-62.5,4,2,1,80,0,0\n" + - "17906,-55.5555555555556,4,2,1,80,0,0\n" + - "18537,-62.5,4,2,1,80,0,0\n" + - "19011,-58.8235294117647,4,2,1,80,0,0\n" + - "19958,-66.6666666666667,4,2,1,80,0,0\n" + - "20274,-66.6666666666667,4,2,1,70,0,0\n" + - "20748,-111.111111111111,4,2,1,70,0,0\n" + - "21537,-62.5,4,2,1,90,0,0\n" + - "22958,-58.8235294117647,4,2,1,90,0,0\n" + - "24064,-62.5,4,2,1,90,0,0\n" + - "25485,-55.5555555555556,4,2,1,90,0,0\n" + - "26590,-62.5,4,2,1,90,0,0\n" + - "29116,-45.4545454545455,4,2,1,90,0,0\n" + - "29748,-50,4,2,1,90,0,0\n" + - "30379,-50,4,2,1,75,0,0\n" + - "30853,-76.9230769230769,4,2,1,75,0,0\n" + - "31327,-76.9230769230769,4,2,1,70,0,0\n" + - "31642,-76.9230769230769,4,2,1,60,0,0\n" + - "31958,-76.9230769230769,4,2,0,5,0,0\n" + - "\n" + - "\n" + - "[Colours]\n" + - "Combo1 : 72,164,255\n" + - "Combo2 : 128,128,192\n" + - "\n" + - "[HitObjects]\n" + - "256,192,1011,5,8,0:2:0:0:\n" + - "351,323,1327,5,6,1:2:0:0:\n" + - "256,292,1642,1,2,0:2:0:0:\n" + - "256,192,1958,1,2,0:2:0:0:\n" + - "351,161,2274,1,2,0:2:0:0:\n" + - "409,242,2590,1,2,0:2:0:0:\n" + - "409,242,3537,5,2,1:2:0:0:\n" + - "103,142,3853,5,6,1:2:0:0:\n" + - "161,223,4169,1,2,0:2:0:0:\n" + - "256,192,4485,1,2,0:2:0:0:\n" + - "256,92,4800,1,2,0:2:0:0:\n" + - "161,61,5116,1,2,0:2:0:0:\n" + - "161,61,5748,6,0,P|172:112|182:171,1,105,0|2,1:2|0:2,0:0:0:0:\n" + - "355,87,6379,5,6,1:2:0:0:\n" + - "486,143,6695,1,2,0:2:0:0:\n" + - "470,288,7011,1,2,0:2:0:0:\n" + - "328,318,7327,1,2,0:2:0:0:\n" + - "256,192,7642,2,0,L|372:181,2,105,2|0|0,0:2|0:0|0:0,0:0:0:0:\n" + - "0,307,8590,6,0,P|32:281|134:282,1,126,2|0,1:2|0:0,0:0:0:0:\n" + - "148,192,8906,6,0,P|193:239|211:334,1,126,6|0,1:2|0:0,0:0:0:0:\n" + - "148,359,9221,1,2,0:2:0:0:\n" + - "148,359,9379,1,0,0:0:0:0:\n" + - "376,327,9537,2,0,L|268:307,1,84,2|0,0:2|0:0,0:0:0:0:\n" + - "507,196,9853,2,0,L|347:225,1,126,2|0,0:2|0:0,0:0:0:0:\n" + - "352,226,10169,6,0,L|369:125,1,84,2|0,0:2|0:0,0:0:0:0:\n" + - "147,190,10485,2,0,L|130:89,1,84\n" + - "467,28,10800,2,0,P|403:52|369:51,1,84\n" + - "101,24,11116,2,0,B|172:11|172:11|228:87,1,126,2|0,0:2|0:0,0:0:0:0:\n" + - "258,56,11432,6,0,L|241:233,1,157.500006008148,6|0,1:2|3:0,0:0:0:0:\n" + - "157,281,11669,1,0,3:0:0:0:\n" + - "157,281,11748,2,0,B|212:293|252:345|252:345|286:274|386:231,1,236.250009012223,2|0,1:2|0:0,0:0:0:0:\n" + - "510,290,12064,6,0,P|520:328|501:361,1,78.7500030040742,2|0,1:2|0:0,0:0:0:0:\n" + - "424,273,12221,2,0,P|405:306|415:344,1,78.7500030040742\n" + - "354,108,12379,6,0,P|363:71|344:38,1,78.7500030040742,2|0,1:2|0:0,0:0:0:0:\n" + - "266,125,12537,2,0,P|247:93|256:55,1,78.7500030040742\n" + - "75,301,12695,5,2,1:2:0:0:\n" + - "75,301,12853,6,0,L|188:272,1,84,2|0,0:2|0:0,0:0:0:0:\n" + - "266,125,13011,2,0,L|233:237,1,84,2|0,1:2|0:0,0:0:0:0:\n" + - "325,372,13169,2,0,L|244:287,1,84,2|0,0:2|0:0,0:0:0:0:\n" + - "13,160,13327,6,0,L|129:178,1,73.4999977569581,2|0,1:2|0:0,0:0:0:0:\n" + - "130,177,13485,2,0,L|111:13,2,146.999995513916,2|2|0,0:2|1:2|0:0,0:0:0:0:\n" + - "512,200,13958,6,0,B|523:186|511:164|511:164|425:159|396:269,1,157.500006008148,2|0,1:2|3:0,0:0:0:0:\n" + - "364,285,14195,1,0,3:0:0:0:\n" + - "364,285,14274,2,0,L|402:-8,1,252,2|0,1:2|0:0,0:0:0:0:\n" + - "486,13,14590,6,0,P|435:41|386:39,1,78.7500030040742,2|0,1:2|0:0,0:0:0:0:\n" + - "274,67,14748,2,0,P|218:49|185:13,1,78.7500030040742\n" + - "279,241,14906,6,0,P|226:215|200:173,1,78.7500030040742,2|0,1:2|0:0,0:0:0:0:\n" + - "129,176,15064,2,0,P|111:120|122:72,1,78.7500030040742\n" + - "19,264,15221,5,2,1:2:0:0:\n" + - "19,264,15379,6,0,P|44:265|17:225,1,89.2500017023087,2|0,0:2|0:0,0:0:0:0:\n" + - "7,29,15537,2,0,L|25:159,1,89.2500017023087,2|0,1:2|0:0,0:0:0:0:\n" + - "147,348,15695,2,0,L|165:218,1,89.2500017023087,2|0,0:2|0:0,0:0:0:0:\n" + - "274,67,15853,6,0,B|222:82|222:82|230:126,1,78.7500030040742,2|0,1:2|0:0,0:0:0:0:\n" + - "398,275,16011,2,0,B|450:260|450:260|420:142,2,157.500006008148,2|2|0,0:2|1:2|1:0,0:0:0:0:\n" + - "116,6,16485,6,0,P|140:101|155:182,1,168,2|0,1:2|3:0,0:0:0:0:\n" + - "159,259,16721,1,0,3:0:0:0:\n" + - "159,259,16800,2,0,B|221:218|324:240|262:280|365:302|428:262,1,252,2|0,1:2|0:0,0:0:0:0:\n" + - "492,230,17116,6,0,L|477:341,1,84,2|0,1:2|0:0,0:0:0:0:\n" + - "398,275,17274,2,0,L|386:358,1,84\n" + - "141,85,17432,6,0,L|153:169,1,84,2|0,1:2|0:0,0:0:0:0:\n" + - "235,131,17590,2,0,L|246:214,1,84\n" + - "488,46,17748,5,2,1:2:0:0:\n" + - "488,46,17906,6,0,B|457:85|457:85|392:68,1,94.499997116089,2|0,0:2|0:0,0:0:0:0:\n" + - "177,12,18064,2,0,B|221:5|221:5|252:44,1,94.499997116089,2|0,1:2|0:0,0:0:0:0:\n" + - "326,190,18221,2,0,L|310:323,1,94.499997116089,2|0,0:2|0:0,0:0:0:0:\n" + - "46,331,18379,5,2,1:2:0:0:\n" + - "46,331,18458,1,0,0:0:0:0:\n" + - "46,331,18537,2,0,L|234:309,2,168,2|2|0,0:2|1:2|0:0,0:0:0:0:\n" + - "504,224,19011,6,0,P|475:299|397:276,1,178.500003404617,2|0,1:2|3:0,0:0:0:0:\n" + - "326,190,19248,1,0,3:0:0:0:\n" + - "326,190,19327,2,0,L|517:206,1,178.500003404617,2|0,1:2|0:0,0:0:0:0:\n" + - "24,133,19642,2,0,L|316:108,1,267.750005106926,2|0,1:2|0:0,0:0:0:0:\n" + - "323,31,19958,6,0,P|302:50|293:121,1,78.7500030040742,2|0,1:2|0:0,0:0:0:0:\n" + - "402,155,20116,2,0,P|423:136|432:65,1,78.7500030040742\n" + - "189,16,20274,5,2,1:2:0:0:\n" + - "158,122,20432,1,2,0:2:0:0:\n" + - "189,228,20590,1,2,0:2:0:0:\n" + - "158,334,20748,6,0,B|205:303|283:319|236:350|314:367|361:336,1,188.999994232178,2|2,1:2|0:2,0:0:0:0:\n" + - "512,304,21221,2,0,P|476:235|438:282,1,141.749995674133,2|0,1:2|0:0,0:0:0:0:\n" + - "430,287,21537,6,0,L|405:120,1,168,6|0,1:2|3:0,0:0:0:0:\n" + - "393,22,21774,1,0,3:0:0:0:\n" + - "417,14,21853,2,0,B|454:30|459:55|459:55|372:22|247:16,1,252,2|0,1:2|0:0,0:0:0:0:\n" + - "213,89,22169,6,0,B|187:122|187:122|201:175,1,84,2|0,1:2|0:0,0:0:0:0:\n" + - "283,178,22327,2,0,B|308:144|308:144|294:91,1,84\n" + - "46,112,22485,2,0,L|257:82,1,168,2|0,1:2|0:0,0:0:0:0:\n" + - "498,44,22800,5,0,1:2:0:0:\n" + - "498,44,22958,6,0,P|476:67|512:58,1,89.2500017023087,2|0,0:2|0:0,0:0:0:0:\n" + - "283,178,23116,2,0,P|306:199|297:163,1,89.2500017023087,2|0,1:2|0:0,0:0:0:0:\n" + - "416,384,23274,2,0,P|437:360|401:369,1,89.2500017023087,2|0,0:2|0:0,0:0:0:0:\n" + - "462,216,23432,5,2,1:2:0:0:\n" + - "462,216,23511,1,0,0:0:0:0:\n" + - "462,216,23590,1,2,0:2:0:0:\n" + - "197,255,23748,2,0,L|484:212,1,267.750005106926,2|0,1:2|0:0,0:0:0:0:\n" + - "512,289,24064,6,0,B|428:308|428:308|367:221,1,168,2|0,1:2|3:0,0:0:0:0:\n" + - "282,155,24300,1,0,3:0:0:0:\n" + - "282,155,24379,2,0,B|458:118|458:118|443:214,1,252,2|0,1:2|0:0,0:0:0:0:\n" + - "307,323,24695,6,0,P|278:294|289:256,1,84,2|0,1:2|0:0,0:0:0:0:\n" + - "380,240,24853,2,0,P|408:268|397:306,1,84\n" + - "197,255,25011,2,0,L|4:221,1,168,2|0,1:2|0:0,0:0:0:0:\n" + - "298,61,25327,5,0,1:2:0:0:\n" + - "298,61,25485,6,0,L|183:81,1,94.499997116089,2|0,0:2|0:0,0:0:0:0:\n" + - "81,5,25642,2,0,L|101:120,1,94.499997116089,2|0,1:2|0:0,0:0:0:0:\n" + - "31,225,25800,2,0,L|146:205,1,94.499997116089,2|0,0:2|0:0,0:0:0:0:\n" + - "304,290,25958,6,0,L|397:306,1,94.499997116089,2|0,1:2|0:0,0:0:0:0:\n" + - "54,379,26116,2,0,L|241:358,2,188.999994232178,2|2|0,0:2|1:2|1:0,0:0:0:0:\n" + - "500,141,26590,6,0,B|419:170|419:170|360:113,1,168,2|0,1:2|3:0,0:0:0:0:\n" + - "441,58,26827,1,0,3:0:0:0:\n" + - "441,58,26906,2,0,L|390:341,1,252,2|0,1:2|0:0,0:0:0:0:\n" + - "352,332,27221,6,0,B|385:353|385:353|452:341,1,84,2|0,1:2|0:0,0:0:0:0:\n" + - "447,216,27379,2,0,B|414:195|414:195|347:207,1,84\n" + - "123,279,27537,2,0,L|314:261,1,168,2|0,1:2|0:0,0:0:0:0:\n" + - "0,291,27853,5,0,1:2:0:0:\n" + - "0,291,28011,6,0,L|20:387,1,84,2|0,0:2|0:0,0:0:0:0:\n" + - "75,15,28169,2,0,L|48:208,1,168,2|2,1:2|0:2,0:0:0:0:\n" + - "344,10,28485,1,2,1:2:0:0:\n" + - "344,10,28564,1,0,0:0:0:0:\n" + - "344,10,28642,1,2,0:2:0:0:\n" + - "491,316,28800,2,0,B|452:378|452:378|412:167,1,252,2|0,1:2|0:0,0:0:0:0:\n" + - "463,139,29116,6,0,B|415:168|415:168|351:149,1,115.50000352478,2|0,1:2|0:0,0:0:0:0:\n" + - "174,30,29274,1,0,3:0:0:0:\n" + - "164,110,29353,1,0,3:0:0:0:\n" + - "152,190,29432,2,0,B|227:204|227:204|206:72|206:72|34:107,1,346.500010574341,2|0,1:2|0:0,0:0:0:0:\n" + - "37,108,29748,6,0,B|15:164|15:164|44:232,1,105,2|0,1:2|0:0,0:0:0:0:\n" + - "163,347,29906,2,0,L|310:307,1,105\n" + - "477,6,30064,2,0,L|436:240,1,210,2|0,1:2|0:0,0:0:0:0:\n" + - "192,31,30379,5,2,1:2:0:0:\n" + - "337,202,30537,1,2,0:2:0:0:\n" + - "163,347,30695,1,2,0:2:0:0:\n" + - "441,213,30853,6,0,L|127:181,1,272.999987503052,2|2,1:2|0:2,0:0:0:0:\n" + - "11,19,31327,2,0,P|114:22|110:134,1,204.749990627289,2|0,1:2|0:0,0:0:0:0:\n" + - "85,178,31642,5,0,1:0:0:0:"