diff --git a/src/main/services/additional-content/maps/local-maps-manager.service.ts b/src/main/services/additional-content/maps/local-maps-manager.service.ts index 03977a9b..d186f953 100644 --- a/src/main/services/additional-content/maps/local-maps-manager.service.ts +++ b/src/main/services/additional-content/maps/local-maps-manager.service.ts @@ -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 { - 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 => { return new Promise((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 { - 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; } } diff --git a/src/main/services/additional-content/maps/song-cache.service.ts b/src/main/services/additional-content/maps/song-cache.service.ts index 998881d5..751b55d8 100644 --- a/src/main/services/additional-content/maps/song-cache.service.ts +++ b/src/main/services/additional-content/maps/song-cache.service.ts @@ -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; + private readonly mapsInfoCache: JsonCache; 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; }; diff --git a/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx index d4b71f31..07a4fc3e 100644 --- a/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx @@ -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; diff --git a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx index cb0cb16e..75248ea6 100644 --- a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx @@ -176,15 +176,15 @@ export const LocalMapsListPanel = forwardRef(({ ve { 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}` }) }

resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" /> diff --git a/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx index a31dbeef..cf69e8cb 100644 --- a/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx @@ -34,7 +34,7 @@ export const DeleteMapsModal: ModalComponent

{t(titleText)}

-

{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).rawInfo._songName })}

+

{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).mapInfo.songName })}

{linked && (

{t(infoText)} diff --git a/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx index bc049861..9d104c3c 100644 --- a/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx @@ -348,8 +348,8 @@ export const EditPlaylistModal: ModalComponent = ({ 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); diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx index 6019b25b..e89e6dce 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx @@ -62,14 +62,14 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver { + public static extractMapDiffs({mapInfo, songDetails, bsvMap}: {mapInfo?: MapInfo, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map { const res = new Map(); 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 { 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 { - if ((mapDetails as BsmLocalMap).rawInfo) { + if ((mapDetails as BsmLocalMap).mapInfo) { return MapItemComponentPropsMapper.fromBsmLocalMap(mapDetails as BsmLocalMap) as MapItemComponentProps; } if ((mapDetails as BsvMapDetail).metadata) { diff --git a/src/shared/models/maps/bsm-local-map.interface.ts b/src/shared/models/maps/bsm-local-map.interface.ts index 757d3e5e..c597b1a6 100644 --- a/src/shared/models/maps/bsm-local-map.interface.ts +++ b/src/shared/models/maps/bsm-local-map.interface.ts @@ -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; } diff --git a/src/shared/models/maps/index.ts b/src/shared/models/maps/index.ts index 24225f20..70597d9b 100644 --- a/src/shared/models/maps/index.ts +++ b/src/shared/models/maps/index.ts @@ -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"; diff --git a/src/shared/models/maps/info/map-info.model.ts b/src/shared/models/maps/info/map-info.model.ts new file mode 100644 index 00000000..d1271688 --- /dev/null +++ b/src/shared/models/maps/info/map-info.model.ts @@ -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; +} diff --git a/src/shared/models/maps/raw-map.model.ts b/src/shared/models/maps/info/raw-map-info-v2.model.ts similarity index 53% rename from src/shared/models/maps/raw-map.model.ts rename to src/shared/models/maps/info/raw-map-info-v2.model.ts index da1b937b..55ce9eb4 100644 --- a/src/shared/models/maps/raw-map.model.ts +++ b/src/shared/models/maps/info/raw-map-info-v2.model.ts @@ -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 { +export interface RawMapInfoDataV2 { _version: string; _songName: string; _songSubName: string; @@ -16,24 +16,24 @@ export interface RawMapInfoData { _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; }; } diff --git a/src/shared/models/maps/info/raw-map-info-v4.model.ts b/src/shared/models/maps/info/raw-map-info-v4.model.ts new file mode 100644 index 00000000..7507252f --- /dev/null +++ b/src/shared/models/maps/info/raw-map-info-v4.model.ts @@ -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; +} diff --git a/src/shared/parsers/maps/map-info.parser.ts b/src/shared/parsers/maps/map-info.parser.ts new file mode 100644 index 00000000..8af15225 --- /dev/null +++ b/src/shared/parsers/maps/map-info.parser.ts @@ -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((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(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}`); +}