From 5e33efbb06132e28c8526f5cc7cc6cfd0c2ed97b Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sat, 12 Oct 2024 18:38:07 +0200 Subject: [PATCH] [feat] add support for the 'info.dat' v4 format --- .../local-maps-manager.service.ts | 41 ++++++---- .../local-maps-list-panel.component.tsx | 2 +- .../maps-row.component.tsx | 47 +++++------ .../delete-maps-modal.component.tsx | 4 +- .../models/maps/bsm-local-map.interface.ts | 4 +- src/shared/models/maps/index.ts | 1 - src/shared/models/maps/info/map-info.model.ts | 42 ++++++++++ .../raw-map-info-v2.model.ts} | 26 +++--- .../models/maps/info/raw-map-info-v4.model.ts | 40 +++++++++ src/shared/parsers/maps/map-info.parser.ts | 82 +++++++++++++++++++ 10 files changed, 230 insertions(+), 59 deletions(-) create mode 100644 src/shared/models/maps/info/map-info.model.ts rename src/shared/models/maps/{raw-map.model.ts => info/raw-map-info-v2.model.ts} (54%) create mode 100644 src/shared/models/maps/info/raw-map-info-v4.model.ts create mode 100644 src/shared/parsers/maps/map-info.parser.ts diff --git a/src/main/services/additional-content/local-maps-manager.service.ts b/src/main/services/additional-content/local-maps-manager.service.ts index b30349aa..6c3c444d 100644 --- a/src/main/services/additional-content/local-maps-manager.service.ts +++ b/src/main/services/additional-content/local-maps-manager.service.ts @@ -1,6 +1,6 @@ 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"; @@ -22,6 +22,7 @@ import { FolderLinkerService } from "../folder-linker.service"; import { allSettled } from "../../../shared/helpers/promise.helpers"; import { splitIntoChunk } from "../../../shared/helpers/array.helpers"; import { IpcService } from "../ipc.service"; +import { parseMapInfoDat } from "../../../shared/parsers/maps/map-info.parser"; export class LocalMapsManagerService { private static instance: LocalMapsManagerService; @@ -84,27 +85,37 @@ export class LocalMapsManagerService { } private async computeMapHash(mapPath: string, rawInfoString: string): Promise { - const mapRawInfo: RawMapInfoData = JSON.parse(rawInfoString); + const mapInfo = parseMapInfoDat(JSON.parse(rawInfoString)); 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: Buffer) => shasum.update(data as unknown as Uint8Array)); 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"); + + const hash = shasum.digest("hex"); + + console.log(mapPath, hash); + + return hash; } private async loadMapInfoFromPath(mapPath: string): Promise { @@ -116,14 +127,14 @@ export class LocalMapsManagerService { } const rawInfoString = await readFile(infoFile, { encoding: "utf-8" }); + const mapInfo = parseMapInfoDat(JSON.parse(rawInfoString)); - const rawInfo: RawMapInfoData = JSON.parse(rawInfoString); - const coverUrl = new URL(`file:///${path.join(mapPath, rawInfo._coverImageFilename)}`).href; - const songUrl = new URL(`file:///${path.join(mapPath, rawInfo._songFilename)}`).href; + const coverUrl = new URL(`file:///${path.join(mapPath, mapInfo.coverImageFilename)}`).href; + const songUrl = new URL(`file:///${path.join(mapPath, mapInfo.songFilename)}`).href; const hash = await this.computeMapHash(mapPath, rawInfoString); - return { rawInfo, coverUrl, songUrl, hash, path: mapPath }; + return { mapInfo, coverUrl, songUrl, hash, path: mapPath }; } private async downloadMapZip(zipUrl: string): Promise<{ zip: StreamZip.StreamZipAsync; zipPath: string }> { @@ -147,7 +158,7 @@ export class LocalMapsManagerService { }); - + } public getMaps(version?: BSVersion): Observable { @@ -261,7 +272,7 @@ export class LocalMapsManagerService { if(!exists){ return null; } return this.loadMapInfoFromPath(mapPath); }).catch(() => null); - + if(map.versions.every(version => version.hash === installedMap?.hash)) { return installedMap; } diff --git a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx index a6f66cf9..421088c1 100644 --- a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx @@ -358,7 +358,7 @@ export const LocalMapsListPanel = forwardRef(({ version, className, filter, sear } const searchCheck = (() => { - return ((map.rawInfo?._songName ?? map.bsaverInfo?.name) || "")?.toLowerCase().includes(search.toLowerCase()) || ((map.rawInfo?._songAuthorName ?? map.bsaverInfo?.metadata?.songAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()) || ((map.rawInfo?._levelAuthorName ?? map.bsaverInfo?.metadata?.levelAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()); + return ((map.mapInfo?.songName ?? map.bsaverInfo?.name) || "")?.toLowerCase().includes(search.toLowerCase()) || ((map.mapInfo?.songAuthorName ?? map.bsaverInfo?.metadata?.songAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()) || ((map.mapInfo?.levelMappers.at(0) ?? map.bsaverInfo?.metadata?.levelAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()); })(); if (!searchCheck) { diff --git a/src/renderer/components/maps-mangement-components/maps-row.component.tsx b/src/renderer/components/maps-mangement-components/maps-row.component.tsx index 0e99fd27..bad81078 100644 --- a/src/renderer/components/maps-mangement-components/maps-row.component.tsx +++ b/src/renderer/components/maps-mangement-components/maps-row.component.tsx @@ -26,43 +26,40 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel if (map.bsaverInfo?.versions[0]?.diffs) { map.bsaverInfo.versions[0].diffs.forEach(diff => { const arr = res.get(diff.characteristic) || []; - const diffName = map.rawInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty; + const diffName = map.mapInfo.difficulties.find(set => set.characteristic === diff.characteristic && set.difficulty === diff.difficulty).difficultyLabel || diff.difficulty; arr.push({ name: diffName, type: diff.difficulty, stars: diff.stars }); res.set(diff.characteristic, arr); }); return res; } - map.rawInfo._difficultyBeatmapSets.forEach(set => { - set._difficultyBeatmaps.forEach(diff => { - const arr = res.get(set._beatmapCharacteristicName) || []; - arr.push({ name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null }); - res.set(set._beatmapCharacteristicName, arr); - }); + map.mapInfo.difficulties.forEach(diff => { + const arr = res.get(diff.characteristic) || []; + arr.push({ name: diff.difficultyLabel || diff.difficulty, type: diff.difficulty, stars: null }); }); return res; }; const renderMapItem = (map: BsmLocalMap) => { - return selected.hash === map.hash)} - diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} - ranked={map.bsaverInfo?.ranked} - autorId={map.bsaverInfo?.uploader?.id} - likes={map.bsaverInfo?.stats?.upvotes} - createdAt={map.bsaverInfo?.createdAt} - onDelete={onMapDelete} - onSelected={onMapSelect} + return selected.hash === map.hash)} + diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} + ranked={map.bsaverInfo?.ranked} + autorId={map.bsaverInfo?.uploader?.id} + likes={map.bsaverInfo?.stats?.upvotes} + createdAt={map.bsaverInfo?.createdAt} + onDelete={onMapDelete} + onSelected={onMapSelect} callBackParam={map} />; }; 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 819d1915..1f79fad3 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 @@ -11,7 +11,7 @@ import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"; import { useService } from "renderer/hooks/use-service.hook"; export const DeleteMapsModal: ModalComponent = ({ resolver, data: { linked, maps } }) => { - + const config = useService(ConfigurationService); const t = useTranslation(); @@ -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/shared/models/maps/bsm-local-map.interface.ts b/src/shared/models/maps/bsm-local-map.interface.ts index 10bb8048..629f48be 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 { BsvMapDetail } from "./beat-saver.model"; -import { RawMapInfoData } from "./raw-map.model"; +import { MapInfo } from "./info/map-info.model"; export interface BsmLocalMap { hash: string; coverUrl: string; songUrl: string; - rawInfo: RawMapInfoData; + mapInfo: MapInfo; bsaverInfo?: BsvMapDetail; path: string; } diff --git a/src/shared/models/maps/index.ts b/src/shared/models/maps/index.ts index fe3e504a..f24c76c9 100644 --- a/src/shared/models/maps/index.ts +++ b/src/shared/models/maps/index.ts @@ -1,2 +1 @@ -export { RawMapInfoData, RawMapDifficulty, RawDifficultySet } from "./raw-map.model"; export { BsvInstant, BsvMapDetail, BsvMapDetailMetadata, BsvMapDifficulty, BsvMapParitySummary, BsvMapStats, BsvMapTestplay, BsvMapVersion, BsvUserDetail } from "./beat-saver.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..2311ce07 --- /dev/null +++ b/src/shared/models/maps/info/map-info.model.ts @@ -0,0 +1,42 @@ +import { BsvMapCharacteristic, BsvMapDifficultyType } from "../beat-saver.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: BsvMapCharacteristic; + difficulty: BsvMapDifficultyType; + 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 54% rename from src/shared/models/maps/raw-map.model.ts rename to src/shared/models/maps/info/raw-map-info-v2.model.ts index 8056233b..8d36829d 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 { BsvMapCharacteristic, BsvMapDifficultyType } from "./beat-saver.model"; +import { BsvMapCharacteristic, BsvMapDifficultyType } from "../beat-saver.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: BsvMapCharacteristic; - _difficultyBeatmaps: RawMapDifficulty[]; + _difficultyBeatmaps: RawMapDifficultyV2[]; } -export interface RawMapDifficulty { +interface RawMapDifficultyV2 { _difficulty: BsvMapDifficultyType; - _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..81029665 --- /dev/null +++ b/src/shared/models/maps/info/raw-map-info-v4.model.ts @@ -0,0 +1,40 @@ +import { BsvMapCharacteristic, BsvMapDifficultyType } from "../beat-saver.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: BsvMapCharacteristic; + difficulty: BsvMapDifficultyType; + 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}`); +}