mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feat-607] add support for map info v4
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsvMapDetail, RawMapInfoData } from "shared/models/maps";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { BSLocalVersionService } from "../../bs-local-version.service";
|
||||
import { InstallationLocationService } from "../../installation-location.service";
|
||||
import { UtilsService } from "../../utils.service";
|
||||
import crypto from "crypto";
|
||||
import crypto, { BinaryLike } from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
@@ -26,6 +26,10 @@ import { SongCacheService } from "./song-cache.service";
|
||||
import { pathToFileURL } from "url";
|
||||
import { sToMs } from "../../../../shared/helpers/time.helpers";
|
||||
import { FieldRequired } from "shared/helpers/type.helpers";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { parseMapInfoDat } from "shared/parsers/maps/map-info.parser";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
@@ -97,24 +101,35 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
private async computeMapHash(mapPath: string, rawInfoString: string): Promise<string> {
|
||||
const mapRawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if(!mapInfo || error) {
|
||||
log.error(`Unable to cumpute hash, cannot parse map info at ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Unable to cumpute hash, cannot parse map info at ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const shasum = crypto.createHash("sha1");
|
||||
shasum.update(rawInfoString);
|
||||
|
||||
const hashFile = (filePath: string): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("data", data => shasum.update(data));
|
||||
stream.on("data", data => shasum.update(data as BinaryLike));
|
||||
stream.on("error", reject);
|
||||
stream.on("close", resolve);
|
||||
});
|
||||
};
|
||||
|
||||
for (const set of mapRawInfo._difficultyBeatmapSets) {
|
||||
for (const diff of set._difficultyBeatmaps) {
|
||||
const diffFilePath = path.join(mapPath, diff._beatmapFilename);
|
||||
for (const diff of mapInfo.difficulties) {
|
||||
if(diff.beatmapFilename){
|
||||
const diffFilePath = path.join(mapPath, diff.beatmapFilename);
|
||||
await hashFile(diffFilePath);
|
||||
}
|
||||
|
||||
if(diff.lightshowDataFilename) {
|
||||
const lightshowFilePath = path.join(mapPath, diff.lightshowDataFilename);
|
||||
await hashFile(lightshowFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return shasum.digest("hex");
|
||||
@@ -122,16 +137,16 @@ export class LocalMapsManagerService {
|
||||
|
||||
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
|
||||
const getUrlsAndReturn = (rawInfo: RawMapInfoData, hash: string, mapPath: string) => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, rawInfo._coverImageFilename)).href;
|
||||
const songUrl = pathToFileURL(path.join(mapPath, rawInfo._songFilename)).href;
|
||||
return { rawInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) } as BsmLocalMap;
|
||||
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string): BsmLocalMap => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, mapInfo.coverImageFilename)).href;
|
||||
const songUrl = pathToFileURL(path.join(mapPath, mapInfo.songFilename)).href;
|
||||
return { mapInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) };
|
||||
};
|
||||
|
||||
const cachedInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
const cachedMapInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
|
||||
if (cachedInfos) {
|
||||
return getUrlsAndReturn(cachedInfos.rawInfo, cachedInfos.hash, mapPath);
|
||||
if (cachedMapInfos) {
|
||||
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath);
|
||||
}
|
||||
|
||||
const files = await getFilesInFolder(mapPath);
|
||||
@@ -142,10 +157,16 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
const rawInfoString = await readFile(infoFile, { encoding: "utf-8" });
|
||||
const rawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if (error) {
|
||||
log.error(`Cannot parse map info.dat. Map path: ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Cannot read map info.dat. Map path: ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const hash = await this.computeMapHash(mapPath, rawInfoString);
|
||||
|
||||
return getUrlsAndReturn(rawInfo, hash, mapPath);
|
||||
return getUrlsAndReturn(mapInfo, hash, mapPath);
|
||||
}
|
||||
|
||||
private async downloadMapZip(zipUrl: string): Promise<{ zip: StreamZip.StreamZipAsync; zipPath: string }> {
|
||||
@@ -193,7 +214,7 @@ export class LocalMapsManagerService {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.songCache.setMapInfoFromDirname(path.basename(mapPath), { rawInfo: mapInfo.rawInfo, hash: mapInfo.hash });
|
||||
this.songCache.setMapInfoFromDirname(path.basename(mapPath), { mapInfo: mapInfo.mapInfo, hash: mapInfo.hash });
|
||||
|
||||
progression.loaded++;
|
||||
observer.next(progression);
|
||||
@@ -267,8 +288,8 @@ export class LocalMapsManagerService {
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
if (hashs.includes(mapInfo.hash)) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo && hashs.includes(mapInfo.hash)) {
|
||||
await deleteFolder(mapPath);
|
||||
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
|
||||
progress.current++;
|
||||
@@ -296,8 +317,8 @@ export class LocalMapsManagerService {
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
if (mapInfo.hash === hash) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo?.hash === hash) {
|
||||
return mapInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CACHE_PATH } from "main/constants";
|
||||
import { JsonCache } from "main/models/json-cache.class";
|
||||
import path from "path";
|
||||
import { RawMapInfoData } from "shared/models/maps";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export class SongCacheService {
|
||||
|
||||
@@ -14,34 +14,35 @@ export class SongCacheService {
|
||||
return SongCacheService.instance;
|
||||
}
|
||||
|
||||
private readonly RAW_INFOS_CACHE_PATH = path.join(CACHE_PATH, "song-raw-info-cache.json");
|
||||
private readonly MAPS_INFO_CACHE_PATH = path.join(CACHE_PATH, "map-info-cache.json");
|
||||
|
||||
private readonly rawInfosCache: JsonCache<CachedRawInfoWithHash>;
|
||||
private readonly mapsInfoCache: JsonCache<CachedMapInfoWithHash>;
|
||||
|
||||
private constructor(){
|
||||
this.rawInfosCache = new JsonCache(this.RAW_INFOS_CACHE_PATH);
|
||||
console.log(this.MAPS_INFO_CACHE_PATH);
|
||||
this.mapsInfoCache = new JsonCache(this.MAPS_INFO_CACHE_PATH);
|
||||
}
|
||||
|
||||
public getMapInfoFromDirname(dirname: string): CachedRawInfoWithHash {
|
||||
return this.rawInfosCache.get(dirname);
|
||||
public getMapInfoFromDirname(dirname: string): CachedMapInfoWithHash {
|
||||
return this.mapsInfoCache.get(dirname);
|
||||
}
|
||||
|
||||
public getMapInfoFromHash(hash: string): { dirname: string, info: CachedRawInfoWithHash } | undefined {
|
||||
const res = Object.entries(this.rawInfosCache.cache).find(([, info]) => info.hash === hash);
|
||||
public getMapInfoFromHash(hash: string): { dirname: string, info: CachedMapInfoWithHash } | undefined {
|
||||
const res = Object.entries(this.mapsInfoCache.cache).find(([, info]) => info.hash === hash);
|
||||
return res ? { dirname: res[0], info: res[1] } : undefined;
|
||||
}
|
||||
|
||||
public setMapInfoFromDirname(dirname: string, info: CachedRawInfoWithHash): void {
|
||||
this.rawInfosCache.set(dirname, info);
|
||||
public setMapInfoFromDirname(dirname: string, info: CachedMapInfoWithHash): void {
|
||||
this.mapsInfoCache.set(dirname, info);
|
||||
}
|
||||
|
||||
public deleteMapInfoFromDirname(dirname: string): void {
|
||||
this.rawInfosCache.delete(dirname);
|
||||
this.mapsInfoCache.delete(dirname);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type CachedRawInfoWithHash = {
|
||||
export type CachedMapInfoWithHash = {
|
||||
hash: string;
|
||||
rawInfo: RawMapInfoData;
|
||||
mapInfo: MapInfo;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { GlowEffect } from "../../shared/glow-effect.component";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
import formatDuration from "format-duration";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
@@ -302,9 +303,10 @@ function isFitVerified(filter: MapFilter, verified: boolean): boolean {
|
||||
return verified;
|
||||
}
|
||||
|
||||
function isFitSearch(search: string, {songName, songAuthorName, levelAuthorName}: {songName: string, songAuthorName: string, levelAuthorName: string}): boolean {
|
||||
function isFitSearch(search: string, {songName, songAuthorName, levelMappers}: {songName: MapInfo["songName"], songAuthorName: MapInfo["songAuthorName"], levelMappers: MapInfo["levelMappers"]}): boolean {
|
||||
if (!search) { return true; }
|
||||
return songName?.toLowerCase().includes(search.toLowerCase()) || songAuthorName?.toLowerCase().includes(search.toLowerCase()) || levelAuthorName?.toLowerCase().includes(search.toLowerCase());
|
||||
if(levelMappers?.some(mapper => mapper?.toLowerCase().includes(search.toLowerCase()))) { return true; }
|
||||
return songName?.toLowerCase().includes(search.toLowerCase()) || songAuthorName?.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
|
||||
export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap, search: string }): boolean => {
|
||||
@@ -323,7 +325,7 @@ export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilte
|
||||
if (!isFitRanked(filter, map.songDetails?.ranked || map.songDetails?.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, map.songDetails?.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.songDetails?.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.rawInfo?._songName, songAuthorName: map.rawInfo?._songAuthorName, levelAuthorName: map.rawInfo?._levelAuthorName})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.mapInfo?.songName, songAuthorName: map.mapInfo?.songAuthorName, levelMappers: map.mapInfo?.levelMappers})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -344,7 +346,7 @@ export const isBsvMapFitMapFilter = ({filter, map, search}: { filter: MapFilter,
|
||||
if (!isFitRanked(filter, map.ranked || map.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, !!map.curator)) { return false; }
|
||||
if (!isFitVerified(filter, !!map.curatedAt)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelAuthorName: map.metadata.levelAuthorName})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelMappers: [map.metadata.levelAuthorName]})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -364,12 +366,12 @@ export const isSongDetailsFitMapFilter = ({filter, map, search}: { filter: MapFi
|
||||
if (!isFitRanked(filter, map.ranked || map.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, map.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelAuthorName: map.uploader.name})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelMappers: [map.uploader.name]})) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
export const isMapFitFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap | BsvMapDetail | SongDetails, search: string }): boolean => {
|
||||
if ((map as BsmLocalMap)?.rawInfo) { return isLocalMapFitMapFilter({filter, map: (map as BsmLocalMap), search}); }
|
||||
if ((map as BsmLocalMap)?.mapInfo) { return isLocalMapFitMapFilter({filter, map: (map as BsmLocalMap), search}); }
|
||||
if ((map as BsvMapDetail)?.metadata) { return isBsvMapFitMapFilter({filter, map: (map as BsvMapDetail), search}); }
|
||||
if ((map as SongDetails).hash) { return isSongDetailsFitMapFilter({filter, map: (map as SongDetails), search}); }
|
||||
return false;
|
||||
|
||||
+5
-5
@@ -176,15 +176,15 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
|
||||
<MapItem
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
title={map.mapInfo.songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
autor={map.mapInfo.levelMappers.at(0)}
|
||||
songAutor={map.mapInfo.songAuthorName}
|
||||
bpm={map.mapInfo.beatsPerMinute}
|
||||
duration={map.songDetails?.duration}
|
||||
selected={renderableMap.selected}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const DeleteDuplicateMapsModal: ModalComponent<void, { maps: BsmLocalMap[
|
||||
<p>{
|
||||
multiple
|
||||
? t("modals.maps-actions.delete-duplicate-maps.desc-plural", { nb: `${maps.length}` })
|
||||
: t("modals.maps-actions.delete-duplicate-maps.desc", { map: `${maps.at(0).rawInfo._songName}` })
|
||||
: t("modals.maps-actions.delete-duplicate-maps.desc", { map: `${maps.at(0).mapInfo.songName}` })
|
||||
}</p>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4 h-8">
|
||||
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DeleteMapsModal: ModalComponent<void, { linked: boolean; maps: BsmL
|
||||
<form className="text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t(titleText)}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
<p className="max-w-sm w-full">{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).rawInfo._songName })}</p>
|
||||
<p className="max-w-sm w-full">{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).mapInfo.songName })}</p>
|
||||
{linked && (
|
||||
<p className="text-sm italic mt-2 cursor-help w-fit" title={t(infoTitleText)}>
|
||||
{t(infoText)}
|
||||
|
||||
+2
-2
@@ -348,8 +348,8 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
|
||||
const { map } = playlistMap;
|
||||
|
||||
if((map as BsmLocalMap).rawInfo?._levelAuthorName){
|
||||
mappersSet.add((map as BsmLocalMap).rawInfo._levelAuthorName);
|
||||
if((map as BsmLocalMap).mapInfo?.levelMappers){
|
||||
(map as BsmLocalMap).mapInfo.levelMappers.forEach(mapper => mappersSet.add(mapper));
|
||||
}
|
||||
else if((map as SongDetails).uploader?.name){
|
||||
mappersSet.add((map as SongDetails).uploader.name);
|
||||
|
||||
+5
-5
@@ -62,14 +62,14 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
<MapItem
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
title={map.mapInfo.songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
autor={map.mapInfo.levelMappers.at(0)}
|
||||
songAutor={map.mapInfo.songAuthorName}
|
||||
bpm={map.mapInfo.beatsPerMinute}
|
||||
duration={map.songDetails?.duration}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
import { MapItemComponentProps } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps";
|
||||
import { BsvMapDetail, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export type ParsedMapDiff = { name: SongDiffName; libelle: string; stars: number, nps: number, njs: number };
|
||||
|
||||
export abstract class MapItemComponentPropsMapper {
|
||||
|
||||
public static extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
public static extractMapDiffs({mapInfo, songDetails, bsvMap}: {mapInfo?: MapInfo, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
|
||||
|
||||
if (bsvMap?.versions?.at(0)?.diffs) {
|
||||
@@ -22,20 +23,18 @@ export abstract class MapItemComponentPropsMapper {
|
||||
if (songDetails?.difficulties) {
|
||||
songDetails?.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
const diffName = rawMapInfo?._difficultyBeatmapSets?.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty;
|
||||
const diffName = mapInfo?.difficulties?.find(mapDiff => mapDiff.characteristic === diff.characteristic && mapDiff.difficulty === diff.difficulty)?.difficultyLabel || diff.difficulty;
|
||||
arr.push({ libelle: diffName, name: diff.difficulty, stars: diff.stars, nps: diff.nps, njs: diff.njs });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
if(rawMapInfo?._difficultyBeatmapSets){
|
||||
rawMapInfo._difficultyBeatmapSets.forEach(set => {
|
||||
set._difficultyBeatmaps.forEach(diff => {
|
||||
const arr = res.get(set._beatmapCharacteristicName) || [];
|
||||
arr.push({ libelle: diff._customData?._difficultyLabel || diff._difficulty, name: diff._difficulty, stars: null, nps: null, njs: diff._noteJumpMovementSpeed });
|
||||
res.set(set._beatmapCharacteristicName, arr);
|
||||
});
|
||||
if(mapInfo?.difficulties){
|
||||
mapInfo.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
arr.push({ libelle: diff.difficultyLabel || diff.difficulty, name: diff.difficulty, stars: null, nps: null, njs: diff.noteJumpMovementSpeed });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,14 +44,14 @@ export abstract class MapItemComponentPropsMapper {
|
||||
public static fromBsmLocalMap(map: BsmLocalMap): MapItemComponentProps<BsmLocalMap> {
|
||||
return {
|
||||
hash: map.hash,
|
||||
title: map.rawInfo._songName,
|
||||
title: map.mapInfo.songName,
|
||||
coverUrl: map.coverUrl,
|
||||
songUrl: map.songUrl,
|
||||
autor: map.rawInfo._levelAuthorName,
|
||||
songAutor: map.rawInfo._songAuthorName,
|
||||
bpm: map.rawInfo._beatsPerMinute,
|
||||
autor: map.mapInfo.levelMappers.at(0),
|
||||
songAutor: map.mapInfo.songAuthorName,
|
||||
bpm: map.mapInfo.beatsPerMinute,
|
||||
duration: map.songDetails?.duration,
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails }),
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails }),
|
||||
mapId: map.songDetails?.id,
|
||||
ranked: map.songDetails?.ranked,
|
||||
blRanked: map.songDetails?.blRanked,
|
||||
@@ -102,7 +101,7 @@ export abstract class MapItemComponentPropsMapper {
|
||||
}
|
||||
|
||||
public static from(mapDetails: BsmLocalMap|BsvMapDetail|SongDetails): MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails> {
|
||||
if ((mapDetails as BsmLocalMap).rawInfo) {
|
||||
if ((mapDetails as BsmLocalMap).mapInfo) {
|
||||
return MapItemComponentPropsMapper.fromBsmLocalMap(mapDetails as BsmLocalMap) as MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails>;
|
||||
}
|
||||
if ((mapDetails as BsvMapDetail).metadata) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RawMapInfoData } from "./raw-map.model";
|
||||
import { MapInfo } from "./info/map-info.model";
|
||||
import { SongDetails } from "./song-details-cache/song-details-cache.model";
|
||||
|
||||
export interface BsmLocalMap {
|
||||
hash: string;
|
||||
coverUrl: string;
|
||||
songUrl: string;
|
||||
rawInfo: RawMapInfoData;
|
||||
mapInfo: MapInfo;
|
||||
songDetails?: SongDetails;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { RawMapInfoData, RawMapDifficulty, RawDifficultySet } from "./raw-map.model";
|
||||
export { BsvInstant, BsvMapDetail, BsvMapDetailMetadata, BsvMapDifficulty, BsvMapParitySummary, BsvMapStats, BsvMapTestplay, BsvMapVersion, BsvUserDetail } from "./beat-saver.model";
|
||||
export { SongDetails, SongDifficulty, SongUploader, SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
import { RawMapInfoDataV2 } from "./raw-map-info-v2.model";
|
||||
import { RawMapInfoDataV4 } from "./raw-map-info-v4.model";
|
||||
|
||||
export type AnyRawMapInfo = RawMapInfoDataV2 | RawMapInfoDataV4;
|
||||
|
||||
// interfaces/mapInfo.ts
|
||||
export interface MapInfo {
|
||||
version: string;
|
||||
songName: string;
|
||||
songSubName?: string;
|
||||
songAuthorName: string;
|
||||
levelMappers: string[];
|
||||
levelLighters: string[];
|
||||
beatsPerMinute: number;
|
||||
shuffle?: number;
|
||||
shufflePeriod?: number;
|
||||
previewStartTime: number;
|
||||
previewDuration: number;
|
||||
songFilename: string;
|
||||
songPreviewFilename: string;
|
||||
coverImageFilename: string;
|
||||
environmentNames: string[];
|
||||
difficulties: MapDifficulty[];
|
||||
}
|
||||
|
||||
export interface MapDifficulty {
|
||||
characteristic: SongDetailDiffCharactertistic;
|
||||
difficulty: SongDiffName;
|
||||
difficultyLabel?: string;
|
||||
beatmapFilename: string;
|
||||
noteJumpMovementSpeed: number;
|
||||
noteJumpStartBeatOffset: number;
|
||||
beatmapColorSchemeIdx?: number;
|
||||
environmentNameIdx?: number;
|
||||
// Additional fields from version 4.0.0
|
||||
beatmapAuthors?: {
|
||||
mappers: string[];
|
||||
lighters: string[];
|
||||
};
|
||||
lightshowDataFilename?: string;
|
||||
}
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
|
||||
export interface RawMapInfoData<T = unknown> {
|
||||
export interface RawMapInfoDataV2 {
|
||||
_version: string;
|
||||
_songName: string;
|
||||
_songSubName: string;
|
||||
@@ -16,24 +16,24 @@ export interface RawMapInfoData<T = unknown> {
|
||||
_environmentName: string;
|
||||
_allDirectionsEnvironmentName: string;
|
||||
_songTimeOffset: number;
|
||||
_customData: T;
|
||||
_difficultyBeatmapSets: RawDifficultySet[];
|
||||
_difficultyBeatmapSets: RawDifficultySetV2[];
|
||||
// Additional fields for 2.1.0
|
||||
_environmentNames?: string[];
|
||||
_colorSchemes?: unknown[];
|
||||
}
|
||||
|
||||
export interface RawDifficultySet {
|
||||
interface RawDifficultySetV2 {
|
||||
_beatmapCharacteristicName: SongDetailDiffCharactertistic;
|
||||
_difficultyBeatmaps: RawMapDifficulty[];
|
||||
_difficultyBeatmaps: RawMapDifficultyV2[];
|
||||
}
|
||||
|
||||
export interface RawMapDifficulty {
|
||||
interface RawMapDifficultyV2 {
|
||||
_difficulty: SongDiffName;
|
||||
_difficultyRank: string;
|
||||
_difficultyRank: number;
|
||||
_beatmapFilename: string;
|
||||
_noteJumpMovementSpeed: number;
|
||||
_noteJumpStartBeatOffset: number;
|
||||
_customData?: RawMapDifficultyCustomData;
|
||||
}
|
||||
|
||||
export interface RawMapDifficultyCustomData {
|
||||
_difficultyLabel?: string;
|
||||
_beatmapColorSchemeIdx?: number;
|
||||
_environmentNameIdx?: number;
|
||||
_customData?: { _difficultyLabel?: string; };
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
|
||||
// interfaces/version4.ts
|
||||
export interface RawMapInfoDataV4 {
|
||||
version: string;
|
||||
song: {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
author: string;
|
||||
};
|
||||
audio: {
|
||||
songFilename: string;
|
||||
songDuration: number;
|
||||
audioDataFilename: string;
|
||||
bpm: number;
|
||||
lufs: number;
|
||||
previewStartTime: number;
|
||||
previewDuration: number;
|
||||
};
|
||||
songPreviewFilename: string;
|
||||
coverImageFilename: string;
|
||||
environmentNames: string[];
|
||||
colorSchemes: unknown[];
|
||||
difficultyBeatmaps: RawMapDifficultyV4[];
|
||||
}
|
||||
|
||||
interface RawMapDifficultyV4 {
|
||||
characteristic: SongDetailDiffCharactertistic;
|
||||
difficulty: SongDiffName;
|
||||
beatmapAuthors: {
|
||||
mappers: string[];
|
||||
lighters: string[];
|
||||
};
|
||||
environmentNameIdx: number;
|
||||
beatmapColorSchemeIdx: number;
|
||||
noteJumpMovementSpeed: number;
|
||||
noteJumpStartBeatOffset: number;
|
||||
beatmapDataFilename: string;
|
||||
lightshowDataFilename: string;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { MapDifficulty, MapInfo, AnyRawMapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { RawMapInfoDataV2 } from "shared/models/maps/info/raw-map-info-v2.model";
|
||||
import { RawMapInfoDataV4 } from "shared/models/maps/info/raw-map-info-v4.model";
|
||||
|
||||
function parseVersion2(data: RawMapInfoDataV2): MapInfo {
|
||||
return {
|
||||
version: data._version,
|
||||
songName: data._songName,
|
||||
songSubName: data._songSubName,
|
||||
songAuthorName: data._songAuthorName,
|
||||
levelMappers: [data._levelAuthorName],
|
||||
levelLighters: [],
|
||||
beatsPerMinute: data._beatsPerMinute,
|
||||
shuffle: data._shuffle,
|
||||
shufflePeriod: data._shufflePeriod,
|
||||
previewStartTime: data._previewStartTime,
|
||||
previewDuration: data._previewDuration,
|
||||
songFilename: data._songFilename,
|
||||
songPreviewFilename: data._songFilename,
|
||||
coverImageFilename: data._coverImageFilename,
|
||||
environmentNames: data._environmentNames || [data._environmentName],
|
||||
difficulties: data._difficultyBeatmapSets.flatMap(set =>
|
||||
set._difficultyBeatmaps.map<MapDifficulty>((diff) => ({
|
||||
characteristic: set._beatmapCharacteristicName,
|
||||
difficulty: diff._difficulty,
|
||||
difficultyLabel: diff._customData?._difficultyLabel,
|
||||
beatmapFilename: diff._beatmapFilename,
|
||||
noteJumpMovementSpeed: diff._noteJumpMovementSpeed,
|
||||
noteJumpStartBeatOffset: diff._noteJumpStartBeatOffset,
|
||||
beatmapColorSchemeIdx: diff._beatmapColorSchemeIdx,
|
||||
environmentNameIdx: diff._environmentNameIdx,
|
||||
}))
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseVersion4(data: RawMapInfoDataV4): MapInfo {
|
||||
return {
|
||||
version: data.version,
|
||||
songName: data.song.title,
|
||||
songSubName: data.song.subTitle,
|
||||
songAuthorName: data.song.author,
|
||||
levelMappers: data.difficultyBeatmaps.flatMap(diff => diff.beatmapAuthors.mappers),
|
||||
levelLighters: data.difficultyBeatmaps.flatMap(diff => diff.beatmapAuthors.lighters),
|
||||
beatsPerMinute: data.audio.bpm,
|
||||
previewStartTime: data.audio.previewStartTime,
|
||||
previewDuration: data.audio.previewDuration,
|
||||
songFilename: data.audio.songFilename,
|
||||
songPreviewFilename: data.songPreviewFilename,
|
||||
coverImageFilename: data.coverImageFilename,
|
||||
environmentNames: data.environmentNames,
|
||||
difficulties: data.difficultyBeatmaps.map<MapDifficulty>(diff => ({
|
||||
characteristic: diff.characteristic,
|
||||
difficulty: diff.difficulty,
|
||||
beatmapFilename: diff.beatmapDataFilename,
|
||||
noteJumpMovementSpeed: diff.noteJumpMovementSpeed,
|
||||
noteJumpStartBeatOffset: diff.noteJumpStartBeatOffset,
|
||||
beatmapColorSchemeIdx: diff.beatmapColorSchemeIdx,
|
||||
environmentNameIdx: diff.environmentNameIdx,
|
||||
beatmapAuthors: diff.beatmapAuthors,
|
||||
lightshowDataFilename: diff.lightshowDataFilename,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseMapInfoDat(info: AnyRawMapInfo): MapInfo | never {
|
||||
const version = (info as RawMapInfoDataV2)?._version || (info as RawMapInfoDataV4)?.version;
|
||||
|
||||
if(!version) {
|
||||
throw new Error('Cannot determine info.dat version');
|
||||
}
|
||||
|
||||
if (version.startsWith('2.')) {
|
||||
return parseVersion2(info as RawMapInfoDataV2);
|
||||
}
|
||||
|
||||
if (version.startsWith('4.')) {
|
||||
return parseVersion4(info as RawMapInfoDataV4);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported info.dat version: ${version}`);
|
||||
}
|
||||
Reference in New Issue
Block a user