mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
maps managment advancement
This commit is contained in:
@@ -18,7 +18,6 @@ export class LocalMapsManagerService {
|
||||
|
||||
private readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data";
|
||||
private readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
|
||||
private readonly WIP_LEVELS_FOLDER = "CustomWIPLevels";
|
||||
|
||||
private readonly localVersion: BSLocalVersionService;
|
||||
private readonly installLocation: InstallationLocationService;
|
||||
|
||||
@@ -19,14 +19,17 @@ export default function App() {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
pageState.setLocation(location);
|
||||
|
||||
useEffect(() => {
|
||||
themeService.theme$.subscribe(() => {
|
||||
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ return document.documentElement.classList.add('dark'); }
|
||||
document.documentElement.classList.remove('dark');
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
pageState.setLocation(location);
|
||||
}, [location])
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service"
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import VisibilitySensor from "react-visibility-sensor"
|
||||
import { useEffect, useState } from "react"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { Subscription } from "rxjs"
|
||||
import { MapItem, MapItemProps } from "./map-item.component"
|
||||
import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model"
|
||||
|
||||
type Props = {
|
||||
version: BSVersion,
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LocalMapsListPanel({version, className} : Props) {
|
||||
|
||||
const mapsManager = MapsManagerService.getInstance();
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [maps, setMaps] = useState([] as BsmLocalMap[]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
if(isVisible && !maps?.length){
|
||||
subs.push(mapsManager.getMaps(version).subscribe(localMaps => setMaps(() => localMaps)));
|
||||
}
|
||||
|
||||
return () => { subs.forEach(s => s.unsubscribe()); }
|
||||
}, [isVisible])
|
||||
|
||||
const extractMapDiffs = (map: BsmLocalMap): Map<BsvMapCharacteristic, BsvMapDifficultyType[]> => {
|
||||
const res = new Map<BsvMapCharacteristic, BsvMapDifficultyType[]>();
|
||||
map.rawInfo._difficultyBeatmapSets.forEach(set => {
|
||||
set._difficultyBeatmaps.forEach(diff => {
|
||||
const arr = res.get(set._beatmapCharacteristicName) || [];
|
||||
arr.push(diff._difficulty);
|
||||
res.set(set._beatmapCharacteristicName, arr);
|
||||
})
|
||||
})
|
||||
return res;
|
||||
}
|
||||
|
||||
const renderMapItem = (map: BsmLocalMap) => {
|
||||
|
||||
return <MapItem
|
||||
key={map.hash}
|
||||
hash={map.hash}
|
||||
title={[map.rawInfo._songAuthorName, map.rawInfo._songName].join(" - ")}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
duration={null}
|
||||
diffs={extractMapDiffs(map)} mapId={null} qualified={null} ranked={null} autorLink={null}
|
||||
/>;
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<VisibilitySensor onChange={setIsVisible}>
|
||||
<ul className={className}>
|
||||
{maps.map(map => renderMapItem(map))}
|
||||
</ul>
|
||||
</VisibilitySensor>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model"
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { BsmLink } from "../shared/bsm-link.component";
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component";
|
||||
import { BsmButton } from "../shared/bsm-button.component";
|
||||
|
||||
export type MapItemProps = {
|
||||
hash: string,
|
||||
title: string,
|
||||
autor: string,
|
||||
coverUrl: string,
|
||||
songUrl: string,
|
||||
autorLink: string,
|
||||
mapId: string,
|
||||
diffs: Map<BsvMapCharacteristic, BsvMapDifficultyType[]>,
|
||||
qualified: boolean,
|
||||
ranked: boolean,
|
||||
bpm: number,
|
||||
duration: number,
|
||||
onDelete?: (hash: string) => void,
|
||||
onDownload?: (id: string) => void,
|
||||
}
|
||||
|
||||
export function MapItem({hash, title, autor, coverUrl, songUrl, autorLink, mapId, diffs, qualified, ranked, onDelete, onDownload}: MapItemProps) {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const zipUrl = `https://r2cdn.beatsaver.com/${hash}.zip`
|
||||
const previewUrl = `https://skystudioapps.com/bs-viewer/?url=${zipUrl}`
|
||||
|
||||
console.log(diffs)
|
||||
|
||||
const renderAutor = () => {
|
||||
if(!autor){ null; }
|
||||
if(autorLink){
|
||||
return <BsmLink className="text-sm mb-1" href={autorLink} style={{color}}>{autor}</BsmLink>
|
||||
}
|
||||
return <span className="text-sm mb-1 text-gray-500">{autor}</span>
|
||||
}
|
||||
|
||||
const renderDiff = (charac: BsvMapCharacteristic, diff: BsvMapDifficultyType) => {
|
||||
|
||||
const colorPill = diffColors[diff] ?? "";
|
||||
|
||||
return (
|
||||
<li className="h-5 flex justify-center items-center gap-1 rounded-full px-2" style={{backgroundColor: colorPill}}>
|
||||
<BsmIcon className="h-4 w-4" icon="bsMapDifficulty"/>
|
||||
<span className="text-xs mb-[1.5px]">{diff === "ExpertPlus" ? "Expert+" : diff}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="bg-main-color-1 rounded-md flex flex-row h-[120px] min-w-[400px] shrink overflow-hidden grow text-white basis-0">
|
||||
<BsmImage className="" image={coverUrl}/>
|
||||
<div className="h-full flex flex-col grow px-3 min-w-0 shrink">
|
||||
<h1 className={`font-bold overflow-hidden text-ellipsis whitespace-nowrap ${mapId && "cursor-pointer hover:underline"}`} style={{textDecorationColor: color}}>{title}</h1>
|
||||
{renderAutor()}
|
||||
<ol className="flex pb-1 gap-2 scrollbar scrollbar-width-[4px] scrollbar-thumb-black scrollbar-track-transparent" style={{scrollbarGutter: "stable", }}>
|
||||
{Array.from(diffs.entries()).map(([charac, diffs]) => diffs.map(diff => renderDiff(charac, diff)))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className="flex flex-col shrink-0 px-2 pt-1 gap-1">
|
||||
<BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="trash" title="allo" withBar={false}/>
|
||||
<BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="twitch" title="allo" withBar={false}/>
|
||||
<BsmButton className="w-6 h-6 pr-[2px] rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="play" title="allo" withBar={false}/>
|
||||
<BsmLink href={previewUrl} internal>
|
||||
<BsmButton className="w-6 h-6 px-[2px] rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="eye" title="Preview" withBar={false}/>
|
||||
</BsmLink>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const diffColors: Record<BsvMapDifficultyType, string> = {
|
||||
Easy: "#008055",
|
||||
Normal: "#1268a1",
|
||||
Hard: "#bd5500",
|
||||
Expert: "#b52a1c",
|
||||
ExpertPlus: "#7646af"
|
||||
}
|
||||
+4
-12
@@ -1,8 +1,7 @@
|
||||
import { useState } from "react"
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook"
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service"
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { TabNavBar } from "../shared/tab-nav-bar.component"
|
||||
import { LocalMapsListPanel } from "./local-maps-list-panel.component"
|
||||
|
||||
type Props = {
|
||||
oneBlock?: boolean,
|
||||
@@ -10,24 +9,17 @@ type Props = {
|
||||
}
|
||||
|
||||
export function MapsPlaylistsPanel({version, oneBlock = false}: Props) {
|
||||
|
||||
const mapsManager = MapsManagerService.getInstance();
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const maps = useObservable(mapsManager.getMaps(version));
|
||||
|
||||
console.log(maps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!oneBlock && <TabNavBar className="mb-8 w-72" tabsText={["Maps", "Playlists"]} onTabChange={setTabIndex}/>}
|
||||
<div className="w-full h-full bg-main-color-1 rounded-md shadow-black shadow-md overflow-hidden">
|
||||
<div className="w-full h-full bg-main-color-2 rounded-md shadow-black shadow-md overflow-hidden">
|
||||
{oneBlock && <TabNavBar className="!rounded-none shadow-sm" tabsText={["Maps", "Playlists"]} onTabChange={setTabIndex}/>}
|
||||
<div className="w-full h-full min-h-0 flex flex-row items-center transition-transform duration-300" style={{transform: `translate(${-(tabIndex * 100)}%, 0)`}}>
|
||||
<div className="w-full h-full grow bg-red-300 shrink-0 overflow-y-scroll">
|
||||
a
|
||||
</div>
|
||||
<div className="w-full h-full grow bg-green-300 shrink-0">b</div>
|
||||
<LocalMapsListPanel className="h-full w-full grow shrink-0 flex flex-wrap justify-center content-start gap-2 p-3" version={version}/>
|
||||
<div className="w-full h-full grow shrink-0">b</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ModalComponent } from "renderer/services/modale.service"
|
||||
|
||||
export const IframeModal: ModalComponent<void, string> = ({resolver, data}) => {
|
||||
return (
|
||||
<div className="w-[calc(100vw-250px)] h-[calc(100vh-250px)]">
|
||||
<iframe className="w-full h-full" src={data}></iframe>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -25,9 +25,10 @@ type propsType = {
|
||||
typeColor?:BsmButtonType,
|
||||
color?: string,
|
||||
title?: string,
|
||||
iconColor?: string,
|
||||
}
|
||||
|
||||
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title}: propsType) {
|
||||
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor}: propsType) {
|
||||
|
||||
const t = useTranslation();
|
||||
const secondColor = useThemeColor("second-color");
|
||||
@@ -52,7 +53,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
|
||||
<OutsideClickHandler onOutsideClick={e => onClickOutside && onClickOutside(e)}>
|
||||
<div onClick={onClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, backgroundColor: primaryColor || color}}>
|
||||
{ image && <BsmImage image={image} className={imgClassName}/> }
|
||||
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"}/> }
|
||||
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{color: iconColor}}/> }
|
||||
{text && (type === "submit" ? <button type="submit" className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: `${textColor}`})}}>{t(text)}</span>)}
|
||||
{ withBar && (
|
||||
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{color: secondColor}}>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service"
|
||||
import { ModalService } from "renderer/services/modale.service";
|
||||
import { IframeModal } from "../modal/modal-types/iframe-modal.component";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
href?: string,
|
||||
internal?: boolean
|
||||
children?: React.ReactNode,
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export function BsmLink({className, href, children, style, internal}: Props) {
|
||||
|
||||
const linkOpener = LinkOpenerService.getInstance();
|
||||
|
||||
const openLink = () => {
|
||||
linkOpener.open(href, internal);
|
||||
}
|
||||
|
||||
return (
|
||||
<a className={`${className} ${href && "cursor-pointer"}`} onClick={openLink} style={style}>{children}</a>
|
||||
)
|
||||
}
|
||||
@@ -25,9 +25,12 @@ import { ThreeDotsIcon } from "./icons/three-dots-icon.component";
|
||||
import { GitHubIcon } from "./icons/github-icon.component";
|
||||
import { CloseIcon } from "./icons/close-icon.component";
|
||||
import { BsMapDifficultyIcon } from "./icons/bs-map-difficulty-icon.component";
|
||||
import { TwitchIcon } from "./icons/twitch-icon.component";
|
||||
import EyeIcon from "./icons/eye-icon.component";
|
||||
import { PlayIcon } from "./icons/play-icon.component";
|
||||
|
||||
export type BsmIconType = (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
|
||||
@@ -62,6 +65,9 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
|
||||
if(icon === "github"){ return <GitHubIcon className={className} style={style}/> }
|
||||
if(icon === "close"){ return <CloseIcon className={className} style={style}/> }
|
||||
if(icon === "bsMapDifficulty"){ return <BsMapDifficultyIcon className={className} style={style}/> }
|
||||
if(icon === "twitch"){ return <TwitchIcon className={className} style={style}/> }
|
||||
if(icon === "eye"){ return <EyeIcon className={className} style={style}/> }
|
||||
if(icon === "play"){ return <PlayIcon className={className} style={style}/> }
|
||||
return <TrashIcon className={className} style={style}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export default function EyeIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" height="40" width="40" viewBox="0 0 40 40" fill="currentColor">
|
||||
<path d="M20 26.375q3 0 5.104-2.104t2.104-5.104q0-3-2.104-5.104Q23 11.958 20 11.958t-5.104 2.105q-2.104 2.104-2.104 5.104t2.104 5.104Q17 26.375 20 26.375Zm0-2.792q-1.875 0-3.146-1.291-1.271-1.292-1.271-3.125 0-1.875 1.292-3.146T20 14.75q1.875 0 3.146 1.292 1.271 1.291 1.271 3.125 0 1.875-1.292 3.145-1.292 1.271-3.125 1.271Zm0 8.334q-5.833 0-10.625-3.146T2 20.417q-.167-.25-.229-.584-.063-.333-.063-.666 0-.334.063-.688.062-.354.229-.562 2.583-5.209 7.375-8.354Q14.167 6.417 20 6.417t10.625 3.146q4.792 3.145 7.375 8.354.167.208.229.562.063.354.063.688 0 .333-.063.666-.062.334-.229.584-2.583 5.208-7.375 8.354Q25.833 31.917 20 31.917Zm0-12.75Zm0 9.708q4.917 0 9.042-2.646t6.291-7.062q-2.166-4.417-6.291-7.063T20 9.458q-4.917 0-9.042 2.646t-6.333 7.063q2.208 4.416 6.333 7.062T20 28.875Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function PlayIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" height="40" width="40" viewBox="0 0 40 40" fill="currentColor">
|
||||
<path d="M15.458 30.542q-.791.541-1.604.083-.812-.458-.812-1.417V10.625q0-.958.812-1.417.813-.458 1.604.084l14.625 9.291q.75.5.75 1.355 0 .854-.75 1.312Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function TwitchIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M5.7 0L1.4 10.985V55.88h15.284V64h8.597l8.12-8.12h12.418l16.716-16.716V0H5.7zm51.104 36.3L47.25 45.85H31.967l-8.12 8.12v-8.12H10.952V5.73h45.85V36.3zM47.25 16.716v16.716h-5.73V16.716h5.73zm-15.284 0v16.716h-5.73V16.716h5.73z" fill="currentColor" fillRule="evenodd"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { IframeModal } from "renderer/components/modal/modal-types/iframe-modal.component";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ModalService } from "./modale.service";
|
||||
|
||||
export class LinkOpenerService{
|
||||
|
||||
private static instance: LinkOpenerService;
|
||||
|
||||
private readonly ipcService: IpcService
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly modals: ModalService;
|
||||
|
||||
public static getInstance(): LinkOpenerService{
|
||||
if(!LinkOpenerService.instance){ LinkOpenerService.instance = new LinkOpenerService(); }
|
||||
@@ -13,9 +16,14 @@ export class LinkOpenerService{
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.modals = ModalService.getInsance();
|
||||
}
|
||||
|
||||
public open(url: string): void{
|
||||
public open(url: string, internal?: boolean): void{
|
||||
if(internal){
|
||||
this.modals.openModal(IframeModal, url);
|
||||
return;
|
||||
}
|
||||
this.ipcService.sendLazy("new-window", {args: url});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export class MapsManagerService {
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMap[]>{
|
||||
console.log("get maps")
|
||||
return new Observable(obs => {
|
||||
this.ipcService.send<BsmLocalMap[], BSVersion>("get-version-maps", {args: version}).then(res => {
|
||||
if(!res.success){ return obs.next(null);}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class PageStateService {
|
||||
}
|
||||
|
||||
public getRoute(): string{
|
||||
return this.location$.value.pathname;
|
||||
return this.location$.value?.pathname;
|
||||
}
|
||||
|
||||
public get state$(): Observable<unknown>{
|
||||
@@ -32,7 +32,7 @@ export class PageStateService {
|
||||
}
|
||||
|
||||
public get route$(): Observable<string>{
|
||||
return this.location$.pipe(map(location => location.pathname));
|
||||
return this.location$.pipe(map(location => location?.pathname));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -115,10 +115,10 @@ export interface BsvMapTestplay {
|
||||
|
||||
export interface BsvMapDifficulty {
|
||||
bombs: number,
|
||||
characteristic: string,
|
||||
characteristic: BsvMapCharacteristic,
|
||||
chroma: boolean,
|
||||
cinema: boolean,
|
||||
difficulty: string,
|
||||
difficulty: BsvMapDifficultyType,
|
||||
events: number,
|
||||
length: number,
|
||||
maxScore: number,
|
||||
@@ -138,4 +138,7 @@ export interface BsvMapParitySummary {
|
||||
errors: number,
|
||||
resets: number,
|
||||
warns: number,
|
||||
}
|
||||
}
|
||||
|
||||
export type BsvMapCharacteristic = ("Standard" | "OneSaber" | "NoArrows" | "90Degree" | "360Degree" | "Lightshow" | "Lawless")
|
||||
export type BsvMapDifficultyType = ("Easy" | "Normal" | "Hard" | "Expert" | "ExpertPlus")
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BsvMapCharacteristic, BsvMapDifficultyType } from "./beat-saver.model"
|
||||
|
||||
export interface RawMapInfoData<T = unknown> {
|
||||
_version: string,
|
||||
_songName: string,
|
||||
@@ -19,12 +21,12 @@ export interface RawMapInfoData<T = unknown> {
|
||||
}
|
||||
|
||||
export interface RawDifficultySet {
|
||||
_beatmapCharacteristicName: string,
|
||||
_beatmapCharacteristicName: BsvMapCharacteristic,
|
||||
_difficultyBeatmaps: RawMapDifficulty[]
|
||||
}
|
||||
|
||||
export interface RawMapDifficulty {
|
||||
_difficulty: string,
|
||||
_difficulty: BsvMapDifficultyType,
|
||||
_difficultyRank: string,
|
||||
_beatmapFilename: string,
|
||||
_noteJumpMovementSpeed: number,
|
||||
|
||||
Reference in New Issue
Block a user