From dcdd60c318d6e8d782b3bfec1dd66707244054aa Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Thu, 29 Feb 2024 03:02:14 +0100 Subject: [PATCH] Basic groundwork for replay viewer --- nise-frontend/package-lock.json | 6 + nise-frontend/package.json | 1 + nise-frontend/src/app/app.component.html | 30 +-- nise-frontend/src/app/app.component.ts | 3 +- .../replay-viewer/decode-beatmap.ts | 57 +++++ .../components/replay-viewer/decode-replay.ts | 70 ++++++ .../replay-viewer/process-replay.ts | 72 ++++++ .../replay-viewer/replay-service.ts | 213 +++++++++++++++++ .../replay-viewer/replay-viewer.component.css | 0 .../replay-viewer.component.html | 5 + .../replay-viewer/replay-viewer.component.ts | 91 +++++++ .../components/replay-viewer/sample-replay.ts | 224 ++++++++++++++++++ 12 files changed, 742 insertions(+), 30 deletions(-) create mode 100644 nise-frontend/src/corelib/components/replay-viewer/decode-beatmap.ts create mode 100644 nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts create mode 100644 nise-frontend/src/corelib/components/replay-viewer/process-replay.ts create mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-service.ts create mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.css create mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.html create mode 100644 nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.ts create mode 100644 nise-frontend/src/corelib/components/replay-viewer/sample-replay.ts diff --git a/nise-frontend/package-lock.json b/nise-frontend/package-lock.json index c9fb831..05f2eed 100644 --- a/nise-frontend/package-lock.json +++ b/nise-frontend/package-lock.json @@ -21,6 +21,7 @@ "chart.js": "^4.4.1", "date-fns": "^3.3.1", "lz-string": "^1.5.0", + "lzma-web": "^3.0.1", "ng2-charts": "^5.0.4", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -8081,6 +8082,11 @@ "lz-string": "bin/bin.js" } }, + "node_modules/lzma-web": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lzma-web/-/lzma-web-3.0.1.tgz", + "integrity": "sha512-sb5cdfd+PLNljK/HUgYzvnz4G7r0GFK8sonyGrqJS0FVyUQjFYcnmU2LqTWFi6r48lH1ZBstnxyLWepKM/t7QA==" + }, "node_modules/magic-string": { "version": "0.30.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", diff --git a/nise-frontend/package.json b/nise-frontend/package.json index b94142f..18c0f8c 100644 --- a/nise-frontend/package.json +++ b/nise-frontend/package.json @@ -24,6 +24,7 @@ "chart.js": "^4.4.1", "date-fns": "^3.3.1", "lz-string": "^1.5.0", + "lzma-web": "^3.0.1", "ng2-charts": "^5.0.4", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/nise-frontend/src/app/app.component.html b/nise-frontend/src/app/app.component.html index c5a7846..7421738 100644 --- a/nise-frontend/src/app/app.component.html +++ b/nise-frontend/src/app/app.component.html @@ -1,31 +1,3 @@
-
- - keisatsu-chan~! - -
-
-

/nise.moe/

- -
- -
-
- - hi, {{this.userService.currentUser?.username}} Logout - - - Login - -
-
-
- -
- v20240225 +
diff --git a/nise-frontend/src/app/app.component.ts b/nise-frontend/src/app/app.component.ts index 4b773cd..f74c9f4 100644 --- a/nise-frontend/src/app/app.component.ts +++ b/nise-frontend/src/app/app.component.ts @@ -3,6 +3,7 @@ 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', @@ -10,7 +11,7 @@ import {FormsModule} from '@angular/forms'; imports: [ RouterLink, FormsModule, - NgIf, RouterOutlet + NgIf, RouterOutlet, ReplayViewerComponent ], templateUrl: './app.component.html', styleUrls: ['./app.component.css'] diff --git a/nise-frontend/src/corelib/components/replay-viewer/decode-beatmap.ts b/nise-frontend/src/corelib/components/replay-viewer/decode-beatmap.ts new file mode 100644 index 0000000..750d0da --- /dev/null +++ b/nise-frontend/src/corelib/components/replay-viewer/decode-beatmap.ts @@ -0,0 +1,57 @@ +export enum HitObjectType { + HitCircle = 1, + Slider = 2, + Spinner = 8 +} + +export type HitObject = { + x: number; + y: number; + time: number; + type: number; + hitSound: number; + objectParams: string; + hitSample: string; +}; + +export function parseHitObjects(beatmap: string): any[] { + const lines = beatmap.split('\n'); + let recording = false; + const hitObjects = []; + + for (const line of lines) { + if (line.trim() === '[HitObjects]') { + recording = true; + continue; + } + + if (!recording) continue; + + if (line.startsWith('[')) break; + + const parts = line.split(','); + if (parts.length < 5) continue; + + const type = parseInt(parts[3], 10); + const hitObject = { + x: parseInt(parts[0], 10), + y: parseInt(parts[1], 10), + time: parseInt(parts[2], 10), + type: getTypeFromFlag(type), + hitSound: parseInt(parts[4], 10), + objectParams: parts[5], + hitSample: parts.length > 6 ? parts[6] : '0:0:0:0:' + }; + + hitObjects.push(hitObject); + } + + return hitObjects; +} + +function getTypeFromFlag(flag: number): HitObjectType | null { + if (flag & HitObjectType.HitCircle) return HitObjectType.HitCircle; + if (flag & HitObjectType.Slider) return HitObjectType.Slider; + if (flag & HitObjectType.Spinner) return HitObjectType.Spinner; + return null; +} diff --git a/nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts b/nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts new file mode 100644 index 0000000..ce8136f --- /dev/null +++ b/nise-frontend/src/corelib/components/replay-viewer/decode-replay.ts @@ -0,0 +1,70 @@ +import {KeyPress, ReplayEvent} from "./replay-viewer.component"; +import LZMA from 'lzma-web' + +export async function getEvents(replayString: string): Promise { + const decompressedData = await decompressData(replayString); + const replayDataStr = new TextDecoder("utf-8").decode(decompressedData); + const trimmedReplayDataStr = replayDataStr.endsWith(',') ? replayDataStr.slice(0, -1) : replayDataStr; + 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; + } + + // Safely cast the integer value to the KeyPress enum + let keyPress = KeyPress[rawKey as unknown as keyof typeof KeyPress]; + + if (keyPress === undefined) { + // TODO: Fix + console.error("Unknown key press:", rawKey); + keyPress = KeyPress.Smoke; + } + + return { + timeDelta, + x, + y, + key: keyPress + }; +} + +async function decompressData(base64Data: string): Promise { + const lzma = new LZMA(); + + const binaryString = atob(base64Data); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + const result = await lzma.decompress(bytes); + + if (typeof result === 'string') { + return new TextEncoder().encode(result); + } else { + return result; + } +} + diff --git a/nise-frontend/src/corelib/components/replay-viewer/process-replay.ts b/nise-frontend/src/corelib/components/replay-viewer/process-replay.ts new file mode 100644 index 0000000..d8a75f5 --- /dev/null +++ b/nise-frontend/src/corelib/components/replay-viewer/process-replay.ts @@ -0,0 +1,72 @@ +import {KeyPress, ReplayEvent} from "./replay-viewer.component"; + +export class ReplayEventProcessed { + x: number; + y: number; + t: number; // Time + key: KeyPress; + + constructor(x: number, y: number, t: number, key: KeyPress) { + this.x = x; + this.y = y; + this.t = t; + this.key = key; + } +} + +export function processReplay(events: ReplayEvent[]): ReplayEventProcessed[] { + 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; + let highestTimeDelta = Number.NEGATIVE_INFINITY; + let lastPositiveFrame: ReplayEvent | null = null; + + let wasInNegativeSection = false; + const lastPositiveFrameData: [number, [number, number]][] = []; + + events.slice(1).forEach((currentFrame, index) => { + 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.key)); + } + wasInNegativeSection = false; + } + + wasInNegativeSection = isInNegativeSection; + + if (!isInNegativeSection) { + pEvents.push(new ReplayEventProcessed(currentFrame.x, currentFrame.y, cumulativeTimeDelta, currentFrame.key)); + } + + 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 new file mode 100644 index 0000000..445b6ec --- /dev/null +++ b/nise-frontend/src/corelib/components/replay-viewer/replay-service.ts @@ -0,0 +1,213 @@ +import { ElementRef, Injectable } from '@angular/core'; +import {HitObject, HitObjectType} from "./decode-beatmap"; +import {ReplayEventProcessed} from "./process-replay"; + +@Injectable({ + providedIn: 'root', +}) +export class ReplayService { + + private hitObjects: HitObject[] = []; + private replayEvents: ReplayEventProcessed[] = []; + + currentTime = 0; + private totalDuration = 0; + private lastRenderTime = 0; + + private isPlaying = false; + private requestId: number | null = null; + + private replayCanvas: ElementRef | null = null; + private ctx: CanvasRenderingContext2D | null = null; + + constructor() {} + + setCanvasElement(canvas: ElementRef) { + this.replayCanvas = canvas; + } + + setCanvasContext(ctx: CanvasRenderingContext2D) { + this.ctx = ctx; + } + + setHitObjects(hitObjects: HitObject[]) { + this.hitObjects = hitObjects; + console.log('Hit objects:', this.hitObjects); + } + + setEvents(events: ReplayEventProcessed[]) { + this.replayEvents = events; + this.calculateTotalDuration(); + console.log('Replay events:', this.replayEvents); + } + + 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; + console.log('Seeking to:', this.currentTime); + } + + private animate(currentTimestamp: number = 0) { + if (!this.isPlaying) return; + + if (!this.lastRenderTime) { + this.lastRenderTime = currentTimestamp; + } + + 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; + + if (this.currentTime > this.totalDuration) { + this.currentTime = this.totalDuration; + this.pause(); + return; + } + + this.drawCurrentEventPosition(); + this.drawHitCircles(); + this.drawSliders(); + + 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 drawCurrentEventPosition() { + const currentEvent = this.getCurrentReplayEvent(); + if (currentEvent) { + this.updateCanvas(currentEvent.x, currentEvent.y); + } + } + + private drawHitCircles() { + if (!this.ctx || !this.replayCanvas) { + console.error('Canvas context not initialized'); + return; + } + + const visibleHitCircles = this.hitObjects.filter(obj => + obj.type === HitObjectType.HitCircle && + this.currentTime >= obj.time - 200 && // Start showing 200ms before + this.currentTime <= obj.time // Hide after its time + ); + + visibleHitCircles.forEach(hitCircle => { + const opacity = this.calculateOpacity(hitCircle.time); + this.drawHitCircle(hitCircle.x, hitCircle.y, opacity); + }); + } + + private drawSliders() { + if (!this.ctx || !this.replayCanvas) { + console.error('Canvas context not initialized'); + return; + } + + const visibleSliders = this.hitObjects.filter(obj => + obj.type === HitObjectType.Slider && + this.currentTime >= obj.time - 200 && // Start showing 200ms before + this.currentTime <= obj.time // Hide after its time + ); + + visibleSliders.forEach(slider => { + const opacity = this.calculateOpacity(slider.time); + this.drawSlider(slider, opacity); + }); + } + + private drawSlider(slider: HitObject, opacity: number) { + this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect + this.ctx!.beginPath(); + this.ctx!.arc(slider.x, slider.y, 25, 0, 2 * Math.PI); // Assuming a radius of 5px + this.ctx!.fill(); + } + + private calculateOpacity(circleTime: number): number { + const timeDifference = circleTime - this.currentTime; + if (timeDifference < 0) { + return 0; // Circle time has passed + } + + // Calculate fade-in effect (0 to 1 over 200ms) + return Math.min(1, (200 - timeDifference) / 200); + } + + private drawHitCircle(x: number, y: number, opacity: number) { + this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect + this.ctx!.beginPath(); + this.ctx!.arc(x, y, 25, 0, 2 * Math.PI); // Assuming a radius of 50px + this.ctx!.fill(); + } + + private updateCanvas(x: number, y: number) { + if (!this.ctx || !this.replayCanvas) { + console.error('Canvas context not initialized'); + return; + } + + this.ctx.clearRect(0, 0, this.replayCanvas.nativeElement.width, this.replayCanvas.nativeElement.height); + this.ctx.fillStyle = '#FFFFFF'; + this.ctx.beginPath(); + this.ctx.arc(x, y, 5, 0, 2 * Math.PI); + this.ctx.fill(); + } + + 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 new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..5c3c08e --- /dev/null +++ b/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.html @@ -0,0 +1,5 @@ + +
Current Time: {{ replayService.currentTime | number: '1.0-0' }}
+
Total Duration: {{ replayService.getTotalDuration() | number: '1.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 new file mode 100644 index 0000000..a6ffa77 --- /dev/null +++ b/nise-frontend/src/corelib/components/replay-viewer/replay-viewer.component.ts @@ -0,0 +1,91 @@ +import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; +import {getEvents} from "./decode-replay"; +import {DecimalPipe, JsonPipe} from "@angular/common"; +import {beatmapData, replayData} from "./sample-replay"; +import {ReplayService} from "./replay-service"; +import {FormsModule} from "@angular/forms"; +import {parseHitObjects} from "./decode-beatmap"; +import {processReplay} from "./process-replay"; + +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; + + /** + * Key being pressed. + */ + key: KeyPress; + +} + +@Component({ + selector: 'app-replay-viewer', + standalone: true, + imports: [ + JsonPipe, + FormsModule, + DecimalPipe + ], + templateUrl: './replay-viewer.component.html', + styleUrl: './replay-viewer.component.css' +}) +export class ReplayViewerComponent implements OnInit, AfterViewInit { + + @ViewChild('replayCanvas') replayCanvas!: ElementRef; + private ctx!: CanvasRenderingContext2D; + + constructor(public replayService: ReplayService) { } + + ngOnInit() { + // Assume getEvents() method returns a promise of ReplayEvent[] + getEvents(replayData).then(events => { + this.replayService.setEvents(processReplay(events)); + this.replayService.setHitObjects(parseHitObjects(beatmapData)) + }); + } + + ngAfterViewInit() { + 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.getIsPlaying()) { + this.replayService.pause(); + } else { + this.replayService.start(); + } + } + + seek(time: number) { + 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 new file mode 100644 index 0000000..f69ae9c --- /dev/null +++ b/nise-frontend/src/corelib/components/replay-viewer/sample-replay.ts @@ -0,0 +1,224 @@ +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:"