diff --git a/src/main/ipcs/bs-mods-ipcs.ts b/src/main/ipcs/bs-mods-ipcs.ts index 20509c0b..e620a87b 100644 --- a/src/main/ipcs/bs-mods-ipcs.ts +++ b/src/main/ipcs/bs-mods-ipcs.ts @@ -3,7 +3,7 @@ import { BsModsManagerService } from "../services/mods/bs-mods-manager.service"; import { UtilsService } from "../services/utils.service"; import { BSVersion } from "shared/bs-version.interface"; import { IpcRequest } from "shared/models/ipc"; -import { Mod } from "shared/models/mods/mod.interface"; +import { BbmFullMod } from "shared/models/mods/mod.interface"; import { InstallModsResult } from "shared/models/mods"; import log from "electron-log"; import { IpcService } from "../services/ipc.service"; @@ -21,7 +21,7 @@ ipc.on("get-installed-mods", (req, reply) => { reply(from(modsManager.getInstalledMods(req.args))); }); -ipcMain.on("install-mods", (event, request: IpcRequest<{ mods: Mod[]; version: BSVersion }>) => { +ipcMain.on("install-mods", (event, request: IpcRequest<{ mods: BbmFullMod[]; version: BSVersion }>) => { const utils = UtilsService.getInstance(); const modsManager = BsModsManagerService.getInstance(); @@ -36,7 +36,7 @@ ipcMain.on("install-mods", (event, request: IpcRequest<{ mods: Mod[]; version: B }); }); -ipcMain.on("uninstall-mods", (event, request: IpcRequest<{ mods: Mod[]; version: BSVersion }>) => { +ipcMain.on("uninstall-mods", (event, request: IpcRequest<{ mods: BbmFullMod[]; version: BSVersion }>) => { const utils = UtilsService.getInstance(); const modsManager = BsModsManagerService.getInstance(); diff --git a/src/main/services/mods/beat-mods-api.service.ts b/src/main/services/mods/beat-mods-api.service.ts index b80b4030..484a4860 100644 --- a/src/main/services/mods/beat-mods-api.service.ts +++ b/src/main/services/mods/beat-mods-api.service.ts @@ -1,19 +1,18 @@ import { BSVersion } from "shared/bs-version.interface"; -import { Mod } from "shared/models/mods/mod.interface"; +import { BbmFullMod, BbmMod, BbmModVersion, BbmPlatform } from "../../../shared/models/mods/mod.interface"; import { RequestService } from "../request.service"; +import { BsStore } from "../../../shared/models/bs-store.enum"; export class BeatModsApiService { private static instance: BeatModsApiService; private readonly requestService: RequestService; - public readonly MODS_REPO_URL = "https://bbm.saera.gay"; - private readonly BEAT_MODS_API_URL = `${this.MODS_REPO_URL}/api`; + public readonly MODS_REPO_URL = "https://beatmods.com"; + private readonly MODS_REPO_API_URL = `${this.MODS_REPO_URL}/api`; - private readonly versionModsCache = new Map(); - private readonly modsHashCache = new Map(); - - private allModsCache: Mod[]; + private readonly versionModsCache = new Map(); + private readonly modsHashCache = new Map(); public static getInstance(): BeatModsApiService { if (!BeatModsApiService.instance) { @@ -26,65 +25,47 @@ export class BeatModsApiService { this.requestService = RequestService.getInstance(); } - private convertToBeatModsMod(mod: Mod): Mod { - - } - private getVersionModsUrl(version: BSVersion): string { - return `${this.BEAT_MODS_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}`; + const platform: BbmPlatform = version.oculus || version.metadata.store === BsStore.OCULUS ? BbmPlatform.OculusPC : BbmPlatform.SteamPC; + return `${this.MODS_REPO_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`; } - private asignDependencies(mod: Mod, mods: Mod[]): Mod { - mod.dependencies = mod.dependencies.map(dep => mods.find(mod => mod.name === dep.name)); - return mod; - } - - private updateModsHashCache(mods: Mod[]): void { + private updateModsHashCache(mods: BbmModVersion[]): void { if(!Array.isArray(mods)){ return; } for (const mod of mods) { - for (const downloads of (mod.downloads ?? [])) { - for (const hashMd5 of (downloads.hashMd5 ?? [])) { - this.modsHashCache.set(hashMd5.hash, mod); - } - } - - for (const dep of (mod.dependencies ?? [])) { - for (const downloads of (dep.downloads ?? [])) { - for (const hashMd5 of (downloads.hashMd5 ?? [])) { - this.modsHashCache.set(hashMd5.hash, dep); - } - } + for (const content of (mod.contentHashes ?? [])) { + this.modsHashCache.set(content.hash, mod); } } } - public async getVersionMods(version: BSVersion): Promise { + public async getVersionMods(version: BSVersion): Promise { if (this.versionModsCache.has(version.BSVersion)) { return this.versionModsCache.get(version.BSVersion); } - return this.requestService.getJSON(this.getVersionModsUrl(version)).then(mods => { - mods = mods.map(mod => this.asignDependencies(mod, mods)); - this.versionModsCache.set(version.BSVersion, mods); + return this.requestService.getJSON<{ mods: {mod: BbmMod, latest: BbmModVersion}[] }>(this.getVersionModsUrl(version)).then(res => { + const fullMods: BbmFullMod[] = res?.mods?.map(mod => ({ mod: mod.mod, version: mod.latest })) ?? []; + this.versionModsCache.set(version.BSVersion, fullMods); - this.updateModsHashCache(mods); + this.updateModsHashCache(fullMods.map(mod => mod.version)); - return mods; + return fullMods; }); } - public getModByHash(hash: string): Promise { + public getModByHash(hash: string): Promise { if (this.modsHashCache.has(hash)) { return Promise.resolve(this.modsHashCache.get(hash)); } - return this.requestService.getJSON(`${this.BEAT_MODS_API_URL}/hashlookup?hash=${hash}`).then(mods => { - this.updateModsHashCache(mods); - return mods.at(0); + return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`).then(res => { + this.updateModsHashCache(res?.modVersions ?? []); + return res?.modVersions?.at(0); }); } } diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index 6a7b3006..e798151e 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -1,5 +1,5 @@ import { BSVersion } from "shared/bs-version.interface"; -import { DownloadLink, InstallModsResult, Mod, ModInstallProgression, UninstallModsResult } from "shared/models/mods"; +import { InstallModsResult, ModInstallProgression, UninstallModsResult } from "shared/models/mods"; import { BeatModsApiService } from "./beat-mods-api.service"; import { BSLocalVersionService } from "../bs-local-version.service"; import path from "path"; @@ -16,6 +16,7 @@ import { sToMs } from "../../../shared/helpers/time.helpers"; import { pathExistsSync } from "fs-extra"; import { BsmZipExtractor } from "../../models/bsm-zip-extractor.class"; import crypto from "crypto"; +import { BbmCategories, BbmFullMod, BbmModVersion } from "../../../shared/models/mods/mod.interface"; export class BsModsManagerService { private static instance: BsModsManagerService; @@ -25,7 +26,7 @@ export class BsModsManagerService { private readonly utilsService: UtilsService; private readonly requestService: RequestService; - private manifestMatches: Mod[]; + private manifestMatches: BbmModVersion[]; private nbModsToInstall = 0; private nbInstalledMods = 0; @@ -48,18 +49,18 @@ export class BsModsManagerService { this.utilsService = UtilsService.getInstance(); } - private async getModFromHash(hash: string): Promise { + private async getModFromHash(hash: string): Promise { const mod = await this.beatModsApi.getModByHash(hash); - if(mod?.name?.toLowerCase() === "bsipa"){ + if(mod?.contentHashes?.some(content => content.path.includes("IPA.exe"))){ return undefined; } return mod; } - private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise { + private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise { const bsPath = await this.bsLocalService.getVersionPath(version); const modsPath = path.join(bsPath, modsDir); @@ -88,7 +89,7 @@ export class BsModsManagerService { } if (filePath.toLowerCase().includes("libs")) { - const manifestIndex = this.manifestMatches.findIndex(m => m.name === mod.name); + const manifestIndex = this.manifestMatches.findIndex(m => m.id === mod.id); if (manifestIndex < 0) { return undefined; @@ -104,7 +105,7 @@ export class BsModsManagerService { return mods.filter(Boolean); } - private async getBsipaInstalled(version: BSVersion): Promise { + private async getBsipaInstalled(version: BSVersion): Promise { const bsPath = await this.bsLocalService.getVersionPath(version); const injectorPath = path.join(bsPath, "Beat Saber_Data", "Managed", "IPA.Injector.dll"); if (!(await pathExist(injectorPath))) { @@ -177,26 +178,23 @@ export class BsModsManagerService { }); } - private getModDownload(mod: Mod, version: BSVersion): DownloadLink { - return mod.downloads.find(download => { - const type = download.type.toLowerCase(); - return type === "universal" || type === this.bsLocalService.getVersionType(version); - }); + private getModDownload(modVersion: BbmModVersion): string { + return `/cdn/mod/${modVersion.zipHash}.zip` } - private async installMod(mod: Mod, version: BSVersion): Promise { - log.info("INSTALL MOD", mod.name, "for version", `${version.BSVersion} - ${version.name}`); - this.utilsService.ipcSend("mod-installed", { success: true, data: { name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100 } }); + private async installMod(mod: BbmFullMod, version: BSVersion): Promise { + log.info("INSTALL MOD", mod.mod.name, "for version", `${version.BSVersion} - ${version.name}`); + this.utilsService.ipcSend("mod-installed", { success: true, data: { name: mod.mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100 } }); - const download = this.getModDownload(mod, version); + const downloadUrl = this.getModDownload(mod.version); - if (!download) { + if (!downloadUrl) { return false; } - log.info("Start download mod zip", mod.name, download.url); - const zip = await this.downloadZip(download.url); - log.info("Mod zip download end", mod.name, download.url); + log.info("Start download mod zip", mod.mod.name, downloadUrl); + const zip = await this.downloadZip(downloadUrl); + log.info("Mod zip download end", mod.mod.name, downloadUrl); if (!zip) { return false; @@ -208,18 +206,18 @@ export class BsModsManagerService { const md5Hash = crypto.createHash("md5") .update(buffer) .digest("hex"); - hashCount += +download.hashMd5.some(md5 => md5.hash === md5Hash); + hashCount += +mod.version.contentHashes.some(content => content.hash === md5Hash); } - if (hashCount !== download.hashMd5.length) { + if (hashCount !== mod.version.contentHashes.length) { return false; } const versionPath = await this.bsLocalService.getVersionPath(version); - const isBSIPA = mod.name.toLowerCase() === "bsipa"; + const isBSIPA = mod.mod.name.toLowerCase() === "bsipa"; const destDir = isBSIPA ? versionPath : path.join(versionPath, ModsInstallFolder.PENDING); - log.info("Start extracting mod zip", mod.name, "to", destDir); + log.info("Start extracting mod zip", mod.mod.name, "to", destDir); const extracted = await zip.extract(destDir) .then(() => true) .catch(e => { @@ -230,7 +228,7 @@ export class BsModsManagerService { zip.close(); }); - log.info("Mod zip extraction end", mod.name, "to", destDir, "success:", extracted); + log.info("Mod zip extraction end", mod.mod.name, "to", destDir, "success:", extracted); const res = isBSIPA ? extracted && @@ -245,23 +243,20 @@ export class BsModsManagerService { return res; } - private isDependency(mod: Mod, selectedMods: Mod[], availableMods: Mod[]) { - return selectedMods.some(m => { - const deps = m.dependencies.map(dep => Array.from(availableMods.values()).find(m => dep.name === m.name)); - if (deps.some(depMod => depMod.name === mod.name)) { - return true; - } - return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name)); - }); + private isDependency(mod: BbmFullMod, selectedMods: BbmFullMod[], availableMods: BbmFullMod[]): boolean { + const selectedDepsIds = selectedMods.flatMap(mod => mod.version.dependencies); + const modsDeps = availableMods.filter(m => selectedDepsIds.includes(m.version.id)); + const modsDepsDepsIds = modsDeps.flatMap(m => m.version.dependencies); + return selectedDepsIds.includes(mod.version.id) || modsDepsDepsIds.includes(mod.version.id); } - private async resolveDependencies(mods: Mod[], version: BSVersion): Promise { + private async resolveDependencies(mods: BbmFullMod[], version: BSVersion): Promise { const availableMods = await this.beatModsApi.getVersionMods(version); return Array.from( - new Map( + new Map( availableMods.reduce((res, mod) => { - if (mod.required || this.isDependency(mod, mods, availableMods)) { - res.push([mod.name, mod]); + if (mod.mod.category === BbmCategories.Core || this.isDependency(mod, mods, availableMods)) { + res.push([mod.mod.id, mod]); } return res; }, []) @@ -269,9 +264,7 @@ export class BsModsManagerService { ); } - private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise { - const download = this.getModDownload(mod, version); - + private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise { const verionPath = await this.bsLocalService.getVersionPath(version); const hasIPAExe = await pathExist(path.join(verionPath, "IPA.exe")); const hasIPADir = await pathExist(path.join(verionPath, "IPA")); @@ -282,37 +275,38 @@ export class BsModsManagerService { await this.executeBSIPA(version, ["--revert", "-n"]); - const promises = download.hashMd5.map(files => { - const file = files.file.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data"); + const promises = mod.version.contentHashes.map(content => { + const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data"); return unlinkPath(path.join(verionPath, file)); }); await Promise.all(promises); } - private async uninstallMod(mod: Mod, version: BSVersion): Promise { + private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise { this.nbUninstalledMods++; - this.utilsService.ipcSend("mod-uninstalled", { success: true, data: { name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100 } }); + this.utilsService.ipcSend("mod-uninstalled", { success: true, data: { name: mod.mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100 } }); - if (mod.name.toLowerCase() === "bsipa") { + if (mod.mod.name.toLowerCase() === "bsipa") { return this.uninstallBSIPA(mod, version); } - const download = this.getModDownload(mod, version); const versionPath = await this.bsLocalService.getVersionPath(version); - const promises = download.hashMd5.map(async files => { - return Promise.all([unlinkPath(path.join(versionPath, files.file)), unlinkPath(path.join(versionPath, "IPA", "Pending", files.file))]); + const promises = mod.version.contentHashes.map(async content => { + return Promise.all([unlinkPath(path.join(versionPath, content.path)), unlinkPath(path.join(versionPath, "IPA", "Pending", content.path))]); }); await Promise.all(promises); } - public getAvailableMods(version: BSVersion): Promise { - return this.beatModsApi.getVersionMods(version); + public getAvailableMods(version: BSVersion): Promise { + return this.beatModsApi.getVersionMods(version).catch(() => { + return [] as BbmFullMod[]; + }); } - public async getInstalledMods(version: BSVersion): Promise { + public async getInstalledMods(version: BSVersion): Promise { this.manifestMatches = []; const bsipa = await this.getBsipaInstalled(version); @@ -322,23 +316,23 @@ export class BsModsManagerService { const dirMods = pluginsMods.flat().concat(libsMods.flat()); - const modsDict = new Map(); + const modsDict = new Map(); if (bsipa) { - modsDict.set(bsipa.name, bsipa); + modsDict.set(bsipa.id, bsipa); } for (const mod of dirMods.flat()) { - if (modsDict.has(mod.name)) { + if (modsDict.has(mod.id)) { continue; } - modsDict.set(mod.name, mod); + modsDict.set(mod.id, mod); } return Array.from(modsDict.values()); } - public async installMods(mods: Mod[], version: BSVersion): Promise { + public async installMods(mods: BbmFullMod[], version: BSVersion): Promise { const deps = await this.resolveDependencies(mods, version); mods.push(...deps); @@ -346,9 +340,9 @@ export class BsModsManagerService { throw "no-mods"; } - const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa"); + const bsipa = mods.find(mod => mod.mod.name.toLowerCase() === "bsipa"); if (bsipa) { - mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa"); + mods = mods.filter(mod => mod.mod.name.toLowerCase() !== "bsipa"); } this.nbModsToInstall = mods.length + (bsipa && 1); @@ -374,7 +368,7 @@ export class BsModsManagerService { }; } - public async uninstallMods(mods: Mod[], version: BSVersion): Promise { + public async uninstallMods(mods: BbmFullMod[], version: BSVersion): Promise { if (!mods?.length) { throw "no-mods"; } @@ -393,16 +387,21 @@ export class BsModsManagerService { } public async uninstallAllMods(version: BSVersion): Promise { - const mods = await this.getInstalledMods(version); + const versionMods = await this.getAvailableMods(version); + const installedMods = await this.getInstalledMods(version); - if (!mods?.length) { + const fullInstalledMods: BbmFullMod[] = installedMods?.map(version => { + return { version, mod: versionMods.find(mod => version.modId === mod.mod.id)?.mod }; + }) ?? []; + + if (!fullInstalledMods?.length) { throw "no-mods"; } - this.nbModsToUninstall = mods.length; + this.nbModsToUninstall = fullInstalledMods.length; this.nbUninstalledMods = 0; - for (const mod of mods) { + for (const mod of fullInstalledMods) { await this.uninstallMod(mod, version); } diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index 78cba41b..fe8f1112 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -49,7 +49,7 @@ export class RequestService { const response = await fetch(url, this.getInitWithOptions(options)); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status} ${url}`); + throw new Error(`HTTP error! status: ${response.status} response: ${await response.text().catch(() => "No response")} url: ${url}`); } return await response.json(); diff --git a/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx b/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx index 32e19b4f..5c38bf81 100644 --- a/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx @@ -3,9 +3,9 @@ import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; -import { Mod } from "shared/models/mods/mod.interface"; +import { BbmMod } from "shared/models/mods/mod.interface"; -export const UninstallModModal: ModalComponent = ({ resolver, data }) => { +export const UninstallModModal: ModalComponent = ({ resolver, data }) => { const mod = data; const t = useTranslation(); diff --git a/src/renderer/components/version-viewer/slides/mods/mod-item.component.tsx b/src/renderer/components/version-viewer/slides/mods/mod-item.component.tsx index 56c24cd7..a306580d 100644 --- a/src/renderer/components/version-viewer/slides/mods/mod-item.component.tsx +++ b/src/renderer/components/version-viewer/slides/mods/mod-item.component.tsx @@ -1,6 +1,5 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; -import { Mod } from "shared/models/mods/mod.interface"; import { CSSProperties, useRef } from "react"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { BsModsManagerService } from "renderer/services/bs-mods-manager.service"; @@ -9,8 +8,9 @@ import { PageStateService } from "renderer/services/page-state.service"; import useDoubleClick from "use-double-click"; import { gt } from "semver"; import { useService } from "renderer/hooks/use-service.hook"; +import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface"; -type Props = { className?: string; mod: Mod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: Mod) => void }; +type Props = { className?: string; mod: BbmFullMod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: BbmFullMod) => void }; export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo }: Props) { const modsManager = useService(BsModsManagerService); @@ -28,7 +28,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele }); const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" }; - const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false; + const isOutDated = installedVersion ? gt(mod.version.modVersion, installedVersion) : false; const uninstall = () => { modsManager.uninstallMod(mod, pageState.getState()); @@ -43,24 +43,24 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele onChange(!isChecked); }; - const isChecked = isDependency || isSelected || mod.required; + const isChecked = isDependency || isSelected || mod.mod.category === BbmCategories.Core; return (
  • - +
    - {mod.name} + {mod.mod.name} {installedVersion || "-"} - {mod.version} + {mod.version.modVersion} - - {mod.description} + + {mod.mod.summary}
    {installedVersion && ( diff --git a/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx b/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx index bc111753..bf8e2825 100644 --- a/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx +++ b/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx @@ -5,17 +5,17 @@ import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-butto import { useTranslation } from "renderer/hooks/use-translation.hook"; import { BsModsManagerService } from "renderer/services/bs-mods-manager.service"; import { PageStateService } from "renderer/services/page-state.service"; -import { Mod } from "shared/models/mods/mod.interface"; +import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface"; import { ModItem } from "./mod-item.component"; import { useService } from "renderer/hooks/use-service.hook"; type Props = { - modsMap: Map; - installed: Map; - modsSelected: Mod[]; - onModChange: (selected: boolean, mod: Mod) => void; - moreInfoMod?: Mod; - onWantInfos: (mod: Mod) => void + modsMap: Map; + installed: Map; + modsSelected: BbmFullMod[]; + onModChange: (selected: boolean, mod: BbmFullMod) => void; + moreInfoMod?: BbmFullMod; + onWantInfos: (mod: BbmFullMod) => void unselectAllMods?: () => void; }; @@ -28,32 +28,29 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn const [filterEnabled, setFilterEnabled] = useState(false); const t = useTranslation(); - const installedModVersion = (key: string, mod: Mod): string => { + const installedModVersion = (key: BbmCategories, mod: BbmFullMod): string => { if (!installed?.get(key)) { return undefined; } - const installedMod = installed.get(key).find(m => m.name === mod.name); + const installedMod = installed.get(key).find(m => m.mod.id === mod.mod.id); if (!installedMod) { return undefined; } - return installedMod.version; + return installedMod.version.modVersion; }; - const isDependency = (mod: Mod): boolean => { - return modsSelected.some(m => { - const deps = m.dependencies.map(dep => - Array.from(modsMap.values()) - .flat() - .find(m => dep.name === m.name) - ); - if (deps.some(depMod => depMod.name === mod.name)) { - return true; - } - return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name)); - }); + const getAvailableMods = (): BbmFullMod[] => { + return Array.from(modsMap.values()).flat(); + } + + const isDependency = (mod: BbmFullMod): boolean => { + const selectedModsDepsIds = modsSelected.flatMap(m => m.version.dependencies); + const modsDeps = getAvailableMods().filter(m => selectedModsDepsIds.includes(m.version.id)); + const modsDepsDepsIds = modsDeps.flatMap(m => m.version.dependencies); + return selectedModsDepsIds.includes(mod.version.id) || modsDepsDepsIds.includes(mod.version.id); }; - const isSelected = (mod: Mod): boolean => modsSelected.some(m => m.name === mod.name); + const isSelected = (mod: BbmFullMod): boolean => modsSelected.some(m => m.mod.id === mod.mod.id); const handleInput = (val: string) => setFilter(val.toLowerCase()); @@ -86,10 +83,10 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn {Array.from(modsMap.keys()).map( key => - modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && ( + modsMap.get(key).some(mod => mod.mod.name.toLowerCase().includes(filter)) && (
      -

      {key}

      - {modsMap.get(key).map(mod => mod.name.toLowerCase().includes(filter) && onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.name === moreInfoMod?.name} />)} +

      {key}

      + {modsMap.get(key).map(mod => mod.mod.name.toLowerCase().includes(filter) && onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.mod.id === moreInfoMod?.mod.id} />)}
    ) )} diff --git a/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx b/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx index 5ab52740..58e3d6e7 100644 --- a/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx +++ b/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx @@ -1,7 +1,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { BsModsManagerService } from "renderer/services/bs-mods-manager.service"; import { BSVersion } from "shared/bs-version.interface"; -import { Mod } from "shared/models/mods/mod.interface"; +import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface"; import { ModsGrid } from "./mods-grid.component"; import { ConfigurationService } from "renderer/services/configuration.service"; import { DefaultConfigKey } from "renderer/config/default-configuration.config"; @@ -31,49 +31,46 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; const ref = useRef(null); const isVisible = useInView(ref, { amount: 0.5 }); - const [modsAvailable, setModsAvailable] = useState(null as Map); - const [modsInstalled, setModsInstalled] = useState(null as Map); - const [modsSelected, setModsSelected] = useState([] as Mod[]); - const [moreInfoMod, setMoreInfoMod] = useState(null as Mod); + const [modsAvailable, setModsAvailable] = useState(null as Map); + const [modsInstalled, setModsInstalled] = useState(null as Map); + const [modsSelected, setModsSelected] = useState([] as BbmFullMod[]); + const [moreInfoMod, setMoreInfoMod] = useState(null as BbmFullMod); const isOnline = useObservable(() => os.isOnline$); const installing = useObservable(() => modsManager.isInstalling$); const downloadRef = useRef(null); const [downloadWith, setDownloadWidth] = useState(0); - const modsToCategoryMap = (mods: Mod[]): Map => { + const modsToCategoryMap = (mods: BbmFullMod[]): Map => { if (!mods) { - return new Map(); + return new Map(); } - const map = new Map(); - mods.forEach(mod => map.set(mod.category, [...(map.get(mod.category) ?? []), mod])); + const map = new Map(); + mods.forEach(mod => map.set(mod.mod.category, [...(map.get(mod.mod.category) ?? []), mod])); return map; }; - const handleModChange = (selected: boolean, mod: Mod) => { + const handleModChange = (selected: boolean, mod: BbmFullMod) => { if (selected) { return setModsSelected([...modsSelected, mod]); } const mods = [...modsSelected]; - mods.splice( - mods.findIndex(m => m.name === mod.name), - 1 - ); + mods.splice(mods.findIndex(m => m.mod.id === mod.mod.id), 1); setModsSelected(mods); }; - const handleMoreInfo = (mod: Mod) => { - if (mod.name === moreInfoMod?.name) { + const handleMoreInfo = (mod: BbmFullMod) => { + if (mod.mod.id === moreInfoMod?.mod.id) { return setMoreInfoMod(null); } setMoreInfoMod(mod); }; const handleOpenMoreInfo = () => { - if (!moreInfoMod?.link) { + if (!moreInfoMod?.mod?.gitUrl) { return; } - linkOpener.open(moreInfoMod.link); + linkOpener.open(moreInfoMod.mod.gitUrl); }; const installMods = () => { @@ -81,14 +78,14 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; return; } const modsToInstall = modsSelected.filter(mod => { - const corespondingMod = modsAvailable.get(mod.category).find(availabeMod => availabeMod._id === mod._id); - const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name); + const corespondingMod = modsAvailable.get(mod.mod.category).find(availabeMod => availabeMod.mod.id === mod.mod.id); + const installedMod = modsInstalled.get(mod.mod.category)?.find(installedMod => installedMod.mod.id === mod.mod.id); - if (corespondingMod?.version && lt(corespondingMod.version, mod.version)) { + if (corespondingMod?.version && lt(corespondingMod.version.modVersion, mod.version.modVersion)) { return false; } - if(installedMod?.version && lt(mod.version, installedMod?.version)){ + if(installedMod?.version && lt(mod.version.modVersion, installedMod?.version.modVersion)){ return false; } @@ -111,8 +108,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; ]).then(([available, installed]) => { const defaultMods = installed?.length ? [] : configService.get("default_mods" as DefaultConfigKey); setModsAvailable(modsToCategoryMap(available)); - setModsSelected(available.filter(m => m.required || defaultMods.some(d => m.name.toLowerCase() === d.toLowerCase()) || installed.some(i => m.name === i.name))); - setModsInstalled(modsToCategoryMap(installed)); + const installedMods: BbmFullMod[] = installed.map(version => { + const mod = available.find(m => m.mod.id === version.modId); + return mod ? { ...mod, version } : null; + }); + setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.toLowerCase()) || installedMods.some(i => m.mod.id === i.mod.id))); + setModsInstalled(modsToCategoryMap(installedMods)); }); }; diff --git a/src/renderer/services/bs-mods-manager.service.ts b/src/renderer/services/bs-mods-manager.service.ts index 2aa9819a..be7c2c53 100644 --- a/src/renderer/services/bs-mods-manager.service.ts +++ b/src/renderer/services/bs-mods-manager.service.ts @@ -3,7 +3,7 @@ import { UninstallModModal } from "renderer/components/modal/modal-types/uninsta import { Observable, BehaviorSubject } from "rxjs"; import { map } from "rxjs/operators"; import { BSVersion } from "shared/bs-version.interface"; -import { InstallModsResult, UninstallModsResult, Mod, ModInstallProgression } from "shared/models/mods"; +import { InstallModsResult, UninstallModsResult, ModInstallProgression } from "shared/models/mods"; import { ProgressionInterface } from "shared/models/progress-bar"; import { IpcService } from "./ipc.service"; import { ModalExitCode, ModalService } from "./modale.service"; @@ -11,6 +11,7 @@ import { NotificationType } from "../../shared/models/notification/notification. import { OsDiagnosticService } from "./os-diagnostic.service"; import { ProgressBarService } from "./progress-bar.service"; import { NotificationService } from "./notification.service"; +import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface"; export class BsModsManagerService { private static instance: BsModsManagerService; @@ -41,15 +42,15 @@ export class BsModsManagerService { this.os = OsDiagnosticService.getInstance(); } - public getAvailableMods(version: BSVersion): Observable { - return this.ipcService.sendV2("get-available-mods", { args: version }); + public getAvailableMods(version: BSVersion): Observable { + return this.ipcService.sendV2("get-available-mods", { args: version }); } - public getInstalledMods(version: BSVersion): Observable { - return this.ipcService.sendV2("get-installed-mods", { args: version }); + public getInstalledMods(version: BSVersion): Observable { + return this.ipcService.sendV2("get-installed-mods", { args: version }); } - public installMods(mods: Mod[], version: BSVersion): Promise { + public installMods(mods: BbmFullMod[], version: BSVersion): Promise { if (this.os.isOffline) { this.notifications.notifyError({ title: "notifications.shared.errors.titles.no-internet", @@ -71,7 +72,7 @@ export class BsModsManagerService { this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" }); this.isInstalling$.next(true); - return this.ipcService.send("install-mods", { args: { mods, version } }).then(res => { + return this.ipcService.send("install-mods", { args: { mods, version } }).then(res => { if (res.success && res.data) { const isFullyInstalled = res.data.nbInstalledMods === res.data.nbModsToInstall; const title = `notifications.mods.install-mods.titles.${isFullyInstalled ? "success" : "warning"}`; @@ -85,12 +86,12 @@ export class BsModsManagerService { this.progressBar.hide(); }); } - public async uninstallMod(mod: Mod, version: BSVersion): Promise { + public async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise { if (!this.progressBar.require()) { return; } - const modalRes = await this.modals.openModal(UninstallModModal, mod); + const modalRes = await this.modals.openModal(UninstallModModal, mod.mod); if (modalRes.exitCode !== ModalExitCode.COMPLETED) { return; diff --git a/src/shared/models/mods/mod.interface.ts b/src/shared/models/mods/mod.interface.ts index 5b7f221b..05edbfbd 100644 --- a/src/shared/models/mods/mod.interface.ts +++ b/src/shared/models/mods/mod.interface.ts @@ -37,6 +37,28 @@ export interface FileHashes { // BBM MOD +export interface BbmFullMod { + mod: BbmMod; + version: BbmModVersion; +} + +export interface BbmMod { + id: number; + name: string; + summary: string; + description: string; + gameName: "BeatSaber"; + category: BbmCategories; + authors: BbmUserAPIResponse[]; + status: BbmStatus; + iconFileName: string; + gitUrl: string; + lastApprovedById: number; + lastUpdatedById: number; + createdAt: Date; + updatedAt: Date; +} + export interface BbmModVersion { id: number; modId: number;