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}`);
+}