mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29426f21b2 | |||
| db1e51b90a | |||
| eba7e55539 | |||
| ef5f6ee5f6 | |||
| 7c185b9d2c | |||
| 8766809fb8 | |||
| a424651f9d | |||
| 5113d72db2 | |||
| 4968022b7e | |||
| 0c65b7b537 | |||
| 8270ec800e | |||
| a70229c80b | |||
| bf4e5aefa4 | |||
| 3c49570f76 | |||
| 0f80f6ae28 | |||
| 066e740a62 | |||
| f296f34e62 | |||
| fb174b4451 | |||
| dfad8a09cc | |||
| 11139bb658 | |||
| 96a3277365 | |||
| 556867d36e | |||
| 9646ce96ad | |||
| fd3fa98650 | |||
| 5b8366a32e | |||
| 37db83580d | |||
| 7120dacf5f | |||
| e1442dc82d | |||
| 3adfd8e6d1 | |||
| 860fe70471 | |||
| cb22f4a59c | |||
| f01638dc1d | |||
| 882bc707ce | |||
| b4d6ca715b | |||
| f7d154bf47 | |||
| cbc2a25f46 | |||
| 493d2627e0 | |||
| b28ae49255 | |||
| 1c40b411c9 | |||
| 7272d41f5c | |||
| cc21992f14 | |||
| 74b13eb5c8 | |||
| 3c1b1f72d4 | |||
| 492e777fa2 | |||
| 6a70c432db | |||
| 5a46a67e1e |
@@ -124,5 +124,8 @@
|
||||
},
|
||||
{
|
||||
"username": "Maximilian"
|
||||
},
|
||||
{
|
||||
"username": "Paul Smith"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "Englisch, USA",
|
||||
"fr-FR": "Französisch",
|
||||
"es-ES": "Spanisch",
|
||||
"it-IT": "Italienisch",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Russisch",
|
||||
"zh-CN": "Vereinfachtes Chinesisch",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "English, USA",
|
||||
"fr-FR": "French",
|
||||
"es-ES": "Spanish",
|
||||
"it-IT": "Italian",
|
||||
"de-DE": "German",
|
||||
"ru-RU": "Russian",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "Inglés, Estados Unidos",
|
||||
"fr-FR": "Francés",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Alemán",
|
||||
"ru-RU": "Ruso",
|
||||
"zh-CN": "Chino simplificado",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "Anglais, États-Unis",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Espagnol",
|
||||
"it-IT": "Italien",
|
||||
"de-DE": "Allemand",
|
||||
"ru-RU": "Russe",
|
||||
"zh-CN": "Chinois simplifié",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "アメリカ英語",
|
||||
"fr-FR": "フランス語",
|
||||
"es-ES": "スペイン語",
|
||||
"it-IT": "イタリア語",
|
||||
"de-DE": "ドイツ語",
|
||||
"ru-RU": "ロシア語",
|
||||
"zh-CN": "簡体字中国",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "영어(미국)",
|
||||
"fr-FR": "프랑스어",
|
||||
"es-ES": "스페인어",
|
||||
"it-IT": "이탈리아어",
|
||||
"de-DE": "독일어",
|
||||
"ru-RU": "러시아어",
|
||||
"zh-CN": "중국어 (간체)",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "Английский, США",
|
||||
"fr-FR": "Французский",
|
||||
"es-ES": "Испанский",
|
||||
"it-IT": "Итальянец",
|
||||
"de-DE": "Немецкий",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "Китайский (упрощенный)",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "簡體中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "美式英語",
|
||||
"fr-FR": "法語",
|
||||
"es-ES": "西班牙語",
|
||||
"it-IT": "義大利語",
|
||||
"de-DE": "德語",
|
||||
"ru-RU": "俄語",
|
||||
"zh-CN": "簡體中文",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
@@ -220,6 +221,7 @@
|
||||
"en-US": "美式英语",
|
||||
"fr-FR": "法语",
|
||||
"es-ES": "西班牙语",
|
||||
"it-IT": "意大利语",
|
||||
"de-DE": "德语",
|
||||
"ru-RU": "俄语",
|
||||
"zh-CN": "简体中文",
|
||||
|
||||
@@ -4,6 +4,8 @@ Your token is a confidential piece of information. Possession of this token allo
|
||||
|
||||
However, you might wonder why it is necessary to provide this token to BSManager. The reason is that BSManager requires the token to continue the download with Oculus. Once you've input the token, it is used exclusively to communicate with Oculus servers to verify that you are the rightful owner of the game.
|
||||
|
||||
# Solution 1 - Using the Oculus app
|
||||
|
||||
## Step 1 - Install and log into the Oculus Rift app
|
||||
- Get the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/)
|
||||
- Install the Oculus Rift app
|
||||
@@ -26,5 +28,23 @@ In the developer tools :
|
||||
|
||||

|
||||
|
||||
# Solution 2 - Using the Oculus website
|
||||
|
||||
## Steps
|
||||
|
||||
1. Log in to your Meta account at https://secure.oculus.com
|
||||
2. Open the developer tools in your web browser:
|
||||
- Chrome/Edge/Firefox: Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> or <kbd>F12</kbd>
|
||||
- Safari: Press <kbd>Option</kbd>+<kbd>Command</kbd>+<kbd>I</kbd>
|
||||
3. Go to the "Application" tab
|
||||
4. Expand "Cookies" in the left sidebar
|
||||
5. Click on `https://secure.oculus.com`
|
||||
6. Find the cookie named `oc_ac_at` (value starts with `OC`)
|
||||
7. Copy the entire `oc_ac_at` cookie value - this is your token
|
||||
|
||||
<img width="500" src="https://github.com/user-attachments/assets/8499035c-4e7c-423d-914d-f35b6a3d77af">
|
||||
|
||||
Paste the token into BSManager or other trusted apps to manage your Beat Saber game and mods. Keep it confidential.
|
||||
|
||||
# Known bugs
|
||||
- [Nothing opens when I press `Ctrl`+`Shift`+`i`.](https://github.com/Zagrios/bs-manager/wiki/Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60i%60.)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "bs-manager",
|
||||
"description": "Manage maps, mods and more for Beat Saber",
|
||||
"main": "./.erb/dll/main.bundle.dev.js",
|
||||
"version": "1.5.0-alpha.6",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"scripts": {
|
||||
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
|
||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0-alpha.6",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0-alpha.6",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0-alpha.6",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"description": "BSManager",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const filterStrings = new Set<string>();
|
||||
export const filterPatterns = new Set<RegExp>();
|
||||
|
||||
// Filter all occulus tokens
|
||||
filterPatterns.add(/FRL\S{10,}/g);
|
||||
filterPatterns.add(/(FRL|OC)\S{10,}/g);
|
||||
|
||||
initLogger();
|
||||
deleteOlestLogs();
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ConfigurationService } from "./configuration.service";
|
||||
import { lstat, rename } from "fs/promises";
|
||||
import log from "electron-log";
|
||||
import { OculusService } from "./oculus.service";
|
||||
import { DownloadLinkType } from "shared/models/mods";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy } from "../helpers/fs.helpers";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
@@ -242,16 +241,6 @@ export class BSLocalVersionService {
|
||||
return version.name ?? version.BSVersion;
|
||||
}
|
||||
|
||||
public getVersionType(version: BSVersion): DownloadLinkType {
|
||||
if (version.steam) {
|
||||
return "steam";
|
||||
}
|
||||
if (version.oculus) {
|
||||
return "oculus";
|
||||
}
|
||||
return "universal";
|
||||
}
|
||||
|
||||
private async getSteamVersion(): Promise<BSVersion> {
|
||||
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
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";
|
||||
import log from "electron-log"
|
||||
|
||||
export class BeatModsApiService {
|
||||
private static instance: BeatModsApiService;
|
||||
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private readonly BEAT_MODS_ALIAS = "https://alias.beatmods.com/aliases.json";
|
||||
public readonly MODS_REPO_URL = "https://beatmods.com";
|
||||
private readonly MODS_REPO_API_URL = `${this.MODS_REPO_URL}/api`;
|
||||
|
||||
private readonly BEAT_MODS_API_URL = "https://beatmods.com/api/v1/";
|
||||
public readonly BEAT_MODS_URL = "https://beatmods.com";
|
||||
|
||||
private readonly aliasesCache = new Map<string, BSVersion[]>();
|
||||
private readonly versionModsCache = new Map<string, Mod[]>();
|
||||
private readonly modsHashCache = new Map<string, Mod>();
|
||||
|
||||
private allModsCache: Mod[];
|
||||
private readonly versionModsCache = new Map<string, BbmFullMod[]>();
|
||||
private readonly modsHashCache = new Map<string, BbmModVersion>();
|
||||
|
||||
public static getInstance(): BeatModsApiService {
|
||||
if (!BeatModsApiService.instance) {
|
||||
@@ -30,101 +27,49 @@ export class BeatModsApiService {
|
||||
}
|
||||
|
||||
private getVersionModsUrl(version: BSVersion): string {
|
||||
return `${this.BEAT_MODS_API_URL}mod?status=approved&gameVersion=${version.BSVersion}&sort=&sortDirection=1`;
|
||||
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 getAllModsUrl(): string {
|
||||
return `${this.BEAT_MODS_API_URL}mod`;
|
||||
}
|
||||
|
||||
private async getVersionAlias(): Promise<Map<string, BSVersion[]>> {
|
||||
if (this.aliasesCache.size) {
|
||||
return this.aliasesCache;
|
||||
}
|
||||
return this.requestService.getJSON<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(({ data: rawAliases }) => {
|
||||
Object.entries(rawAliases).forEach(([key, value]) => {
|
||||
this.aliasesCache.set(
|
||||
key,
|
||||
value.map(s => ({ BSVersion: s } as BSVersion))
|
||||
);
|
||||
});
|
||||
return this.aliasesCache;
|
||||
});
|
||||
}
|
||||
|
||||
private async getAliasOfVersion(version: BSVersion): Promise<BSVersion> {
|
||||
return this.getVersionAlias().then(aliases => {
|
||||
if (Array.from(aliases.keys()).some(k => k === version.BSVersion)) {
|
||||
return version;
|
||||
}
|
||||
const alias = Array.from(aliases.entries()).find(([, value]) => value.find(v => v.BSVersion === version.BSVersion))?.[0];
|
||||
return { BSVersion: alias } as BSVersion;
|
||||
});
|
||||
}
|
||||
|
||||
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<Mod[]> {
|
||||
public async getVersionMods(version: BSVersion): Promise<BbmFullMod[]> {
|
||||
if (this.versionModsCache.has(version.BSVersion)) {
|
||||
return this.versionModsCache.get(version.BSVersion);
|
||||
}
|
||||
|
||||
const alias = await this.getAliasOfVersion(version);
|
||||
return this.requestService.getJSON<{ mods: {mod: BbmMod, latest: BbmModVersion}[] }>(this.getVersionModsUrl(version)).then(({ data }) => {
|
||||
const fullMods: BbmFullMod[] = data?.mods?.map(mod => ({ mod: mod.mod, version: mod.latest })) ?? [];
|
||||
this.versionModsCache.set(version.BSVersion, fullMods);
|
||||
|
||||
return this.requestService.getJSON<Mod[]>(this.getVersionModsUrl(alias)).then(({ data: mods }) => {
|
||||
mods = mods.map(mod => this.asignDependencies(mod, mods));
|
||||
this.versionModsCache.set(version.BSVersion, mods);
|
||||
this.updateModsHashCache(fullMods.map(mod => mod.version));
|
||||
|
||||
this.updateModsHashCache(mods);
|
||||
|
||||
return mods;
|
||||
return fullMods;
|
||||
});
|
||||
}
|
||||
|
||||
public async getAllMods(): Promise<Mod[]> {
|
||||
if (this.allModsCache) {
|
||||
return this.allModsCache;
|
||||
}
|
||||
return this.requestService.getJSON<Mod[]>(this.getAllModsUrl()).then(({ data: mods }) => {
|
||||
this.allModsCache = mods;
|
||||
return this.allModsCache;
|
||||
});
|
||||
}
|
||||
|
||||
public getModByHash(hash: string): Promise<Mod> {
|
||||
public getModByHash(hash: string): Promise<BbmModVersion|undefined> {
|
||||
if (this.modsHashCache.has(hash)) {
|
||||
return Promise.resolve(this.modsHashCache.get(hash));
|
||||
}
|
||||
|
||||
return this.requestService.getJSON<Mod[]>(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(({ data: mods }) => {
|
||||
this.updateModsHashCache(mods);
|
||||
return mods.at(0);
|
||||
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`).then(({ data }) => {
|
||||
this.updateModsHashCache(data?.modVersions ?? []);
|
||||
return data?.modVersions?.at(0);
|
||||
}).catch((e): undefined => {
|
||||
log.error(`Failed to get mod by hash: ${hash}`, e);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { DownloadLink, Mod } from "shared/models/mods";
|
||||
import { BeatModsApiService } from "./beat-mods-api.service";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import path from "path";
|
||||
@@ -19,7 +18,7 @@ import { tryit } from "shared/helpers/error.helpers";
|
||||
import crypto from "crypto";
|
||||
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
||||
import { bsmSpawn } from "main/helpers/os.helpers";
|
||||
import { ExternalMod } from "shared/models/mods/mod.interface";
|
||||
import { BbmFullMod, BbmModVersion, ExternalMod } from "../../../shared/models/mods/mod.interface";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -29,7 +28,7 @@ export class BsModsManagerService {
|
||||
private readonly linuxService: LinuxService;
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private manifestMatches: Mod[];
|
||||
private manifestMatches: BbmModVersion[];
|
||||
|
||||
public static getInstance(): BsModsManagerService {
|
||||
if (!BsModsManagerService.instance) {
|
||||
@@ -45,18 +44,18 @@ export class BsModsManagerService {
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
private async getModFromHash(hash: string): Promise<Mod> {
|
||||
|
||||
private async getModFromHash(hash: string): Promise<BbmModVersion | undefined> {
|
||||
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<Mod[]> {
|
||||
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<BbmModVersion[]> {
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const modsPath = path.join(bsPath, modsDir);
|
||||
|
||||
@@ -85,7 +84,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;
|
||||
@@ -101,7 +100,7 @@ export class BsModsManagerService {
|
||||
return mods.filter(Boolean);
|
||||
}
|
||||
|
||||
private async getBsipaInstalled(version: BSVersion): Promise<Mod> {
|
||||
private async getBsipaInstalled(version: BSVersion): Promise<BbmModVersion> {
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const injectorPath = path.join(bsPath, "Beat Saber_Data", "Managed", "IPA.Injector.dll");
|
||||
if (!(await pathExist(injectorPath))) {
|
||||
@@ -112,15 +111,14 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
|
||||
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
|
||||
zipUrl = path.join(this.beatModsApi.MODS_REPO_URL, zipUrl);
|
||||
|
||||
log.info("Download mod zip", zipUrl);
|
||||
|
||||
const buffer = await lastValueFrom(this.requestService.downloadBuffer(zipUrl))
|
||||
.then(progress => progress.data)
|
||||
.catch(e => {
|
||||
.catch((e: Error) => {
|
||||
log.error("ZIP", "Error while downloading zip", e);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (!buffer) {
|
||||
@@ -129,7 +127,6 @@ export class BsModsManagerService {
|
||||
|
||||
return BsmZipExtractor.fromBuffer(buffer);
|
||||
}
|
||||
|
||||
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean> {
|
||||
|
||||
log.info("executeBSIPA", version?.BSVersion, args);
|
||||
@@ -190,25 +187,22 @@ 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<boolean> {
|
||||
log.info("INSTALL MOD", mod.name, "for version", `${version.BSVersion} - ${version.name}`);
|
||||
private async installMod(mod: BbmFullMod, version: BSVersion): Promise<boolean> {
|
||||
log.info("INSTALL MOD", mod.mod.name, "for version", `${version.BSVersion} - ${version.name}`);
|
||||
|
||||
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;
|
||||
@@ -220,18 +214,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 => {
|
||||
@@ -242,7 +236,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 &&
|
||||
@@ -255,9 +249,7 @@ export class BsModsManagerService {
|
||||
return res;
|
||||
}
|
||||
|
||||
private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise<void> {
|
||||
const download = this.getModDownload(mod, version);
|
||||
|
||||
private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||
const verionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const hasIPAExe = await pathExist(path.join(verionPath, "IPA.exe"));
|
||||
const hasIPADir = await pathExist(path.join(verionPath, "IPA"));
|
||||
@@ -268,34 +260,34 @@ 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<void> {
|
||||
if (mod.name.toLowerCase() === "bsipa") {
|
||||
private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||
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<Mod[]> {
|
||||
return this.beatModsApi.getVersionMods(version);
|
||||
public async getAvailableMods(version: BSVersion): Promise<BbmFullMod[]> {
|
||||
return this.beatModsApi.getVersionMods(version).catch(() => {
|
||||
return [] as BbmFullMod[];
|
||||
});
|
||||
}
|
||||
|
||||
public async getInstalledMods(version: BSVersion): Promise<Mod[]> {
|
||||
public async getInstalledMods(version: BSVersion): Promise<BbmModVersion[]> {
|
||||
this.manifestMatches = [];
|
||||
|
||||
const bsipa = await this.getBsipaInstalled(version);
|
||||
@@ -305,17 +297,17 @@ export class BsModsManagerService {
|
||||
|
||||
const dirMods = pluginsMods.flat().concat(libsMods.flat());
|
||||
|
||||
const modsDict = new Map<string, Mod>();
|
||||
const modsDict = new Map<number, BbmModVersion>();
|
||||
|
||||
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());
|
||||
@@ -423,7 +415,7 @@ export class BsModsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
public installMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
|
||||
const progress = { current: 0, total: mods.length };
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
@@ -434,7 +426,7 @@ export class BsModsManagerService {
|
||||
|
||||
obs.next(progress);
|
||||
|
||||
const bsipa = popElement(mod => mod.name.toLowerCase() === "bsipa", mods);
|
||||
const bsipa = popElement(mod => mod.mod.name.toLowerCase() === "bsipa", mods);
|
||||
|
||||
if(bsipa){
|
||||
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
|
||||
@@ -460,7 +452,7 @@ export class BsModsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public uninstallMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
public uninstallMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
|
||||
const progress = { current: 0, total: mods.length };
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
@@ -485,16 +477,19 @@ export class BsModsManagerService {
|
||||
public uninstallAllMods(version: BSVersion): Observable<Progression> {
|
||||
return new Observable<Progression>(obs => {
|
||||
(async () => {
|
||||
const mods = await this.getInstalledMods(version).catch(err => {
|
||||
log.error(err);
|
||||
return [];
|
||||
});
|
||||
|
||||
const progress = { current: 0, total: mods.length };
|
||||
const versionMods = await this.getAvailableMods(version);
|
||||
const installedMods = await this.getInstalledMods(version);
|
||||
|
||||
const fullInstalledMods: BbmFullMod[] = installedMods?.map(version => {
|
||||
return { version, mod: versionMods.find(mod => version.modId === mod.mod.id)?.mod };
|
||||
}) ?? [];
|
||||
|
||||
const progress = { current: 0, total: fullInstalledMods.length };
|
||||
|
||||
obs.next(progress);
|
||||
|
||||
for (const mod of mods) {
|
||||
for (const mod of fullInstalledMods) {
|
||||
await this.uninstallMod(mod, version);
|
||||
progress.current++;
|
||||
obs.next(progress);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
|
||||
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";
|
||||
|
||||
export const UninstallModModal: ModalComponent<void, Mod> = ({ resolver, options: {data} }) => {
|
||||
const mod = data;
|
||||
const t = useTranslation();
|
||||
|
||||
const desc = mod.name.toLowerCase() === "bsipa" ? "modals.uninstall-mod.description-bsipa" : "modals.uninstall-mod.description";
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
resolver({ exitCode: ModalExitCode.COMPLETED });
|
||||
}}
|
||||
>
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.uninstall-mod.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t(desc, { mod: mod.name })}</p>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
|
||||
<BsmButton
|
||||
typeColor="cancel"
|
||||
className="rounded-md text-center transition-all"
|
||||
onClick={() => {
|
||||
resolver({ exitCode: ModalExitCode.CANCELED });
|
||||
}}
|
||||
withBar={false}
|
||||
text="misc.cancel"
|
||||
/>
|
||||
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" type="submit" withBar={false} text="modals.bs-uninstall.buttons.submit" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { AddIcon } from "./icons/add-icon.component";
|
||||
import { CrossIcon } from "./icons/cross-icon.component";
|
||||
import { FranceIcon } from "./flags/france-icon.component";
|
||||
import { SpainIcon } from "./flags/spain-icon.component";
|
||||
import { ItalyIcon } from "./flags/italy-icon.component";
|
||||
import { UsaIcon } from "./flags/usa-icon.component";
|
||||
import { UkIcon } from "./flags/uk-icon.component";
|
||||
import { TaskIcon } from "./icons/task-icon.component";
|
||||
@@ -70,7 +71,7 @@ import { WarningIcon } from "./icons/warning-icon.component";
|
||||
|
||||
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
|
||||
|
||||
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
|
||||
// TODO : Very ugly very messy, need to find a better way to do this
|
||||
@@ -113,6 +114,9 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
if (icon === "es-ES-flag") {
|
||||
return <SpainIcon className={className} style={style} />;
|
||||
}
|
||||
if (icon === "it-IT-flag") {
|
||||
return <ItalyIcon className={className} style={style} />;
|
||||
}
|
||||
if (icon === "en-US-flag") {
|
||||
return <UsaIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const ItalyIcon = createSvgIcon((props, ref) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" ref={ref} {...props} width="71" height="48">
|
||||
<g fill="none">
|
||||
<path d="M6.65.53A6.36 6.36 0 0 0 .29 6.89v34.27a6.36 6.36 0 0 0 6.36 6.36h17v-47z" fill="#57A863"/>
|
||||
<path d="M23.63.53v47H47v-47z" fill="#FFF"/>
|
||||
<path d="M63.92.53H47v47h16.92a6.36 6.36 0 0 0 6.36-6.36V6.89A6.36 6.36 0 0 0 63.92.53" fill="#ED5565"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -1,21 +1,22 @@
|
||||
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 { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
|
||||
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import useDoubleClick from "use-double-click";
|
||||
import { gt } from "semver";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import striptags from "striptags";
|
||||
import { safeGt } from "shared/helpers/semver.helpers";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
mod: Mod;
|
||||
mod: BbmFullMod;
|
||||
installedVersion: string;
|
||||
isDependency?: boolean;
|
||||
isSelected?: boolean;
|
||||
onChange?: (val: boolean) => void;
|
||||
wantInfo?: boolean;
|
||||
onWantInfo?: (mod: Mod) => void;
|
||||
onWantInfo?: (mod: BbmFullMod) => void;
|
||||
disabled?: boolean;
|
||||
onUninstall?: () => void;
|
||||
};
|
||||
@@ -25,7 +26,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
const themeColor = useThemeColor("second-color");
|
||||
const clickRef = useRef();
|
||||
|
||||
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
|
||||
const isChecked = useMemo(() => isDependency || isSelected || mod.mod.category === BbmCategories.Core, [isDependency, isSelected, mod.mod.category]);
|
||||
|
||||
useDoubleClick({
|
||||
onSingleClick: e => handleWantInfo(e),
|
||||
@@ -39,7 +40,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
}, [isChecked]);
|
||||
|
||||
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
|
||||
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
|
||||
const isOutDated = installedVersion ? safeGt(mod.version.modVersion, installedVersion) : false;
|
||||
|
||||
const handleWantInfo = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -53,19 +54,19 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
return (
|
||||
<li ref={clickRef} className={`${className} group`}>
|
||||
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency || disabled} checked={isChecked} />
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.mod.category === BbmCategories.Core || isDependency || disabled} checked={isChecked} />
|
||||
</div>
|
||||
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{mod.name}
|
||||
{mod.mod.name}
|
||||
</span>
|
||||
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${installedVersion && isOutDated && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>
|
||||
{installedVersion || "-"}
|
||||
</span>
|
||||
<span className="min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{mod.version}
|
||||
{mod.version.modVersion}
|
||||
</span>
|
||||
<span title={mod.description} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{mod.description}
|
||||
<span title={striptags(mod.mod?.description ?? "", { tagReplacementText: " " })} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{striptags(mod.mod?.summary ?? "", { tagReplacementText: " " })}
|
||||
</span>
|
||||
<div className="h-full bg-inherit flex items-center justify-center mr-3 rounded-r-md pr-2 border-t-2 border-b-2 border-r-2 group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{installedVersion && (
|
||||
|
||||
@@ -2,19 +2,19 @@ import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
|
||||
import { ModItem } from "./mod-item.component";
|
||||
|
||||
type Props = {
|
||||
modsMap: Map<string, Mod[]>;
|
||||
installed: Map<string, Mod[]>;
|
||||
modsSelected: Mod[];
|
||||
onModChange: (selected: boolean, mod: Mod) => void;
|
||||
moreInfoMod?: Mod;
|
||||
onWantInfos: (mod: Mod) => void
|
||||
modsMap: Map<BbmCategories, BbmFullMod[]>;
|
||||
installed: Map<BbmCategories, BbmFullMod[]>;
|
||||
modsSelected: BbmFullMod[];
|
||||
onModChange: (selected: boolean, mod: BbmFullMod) => void;
|
||||
moreInfoMod?: BbmFullMod;
|
||||
onWantInfos: (mod: BbmFullMod) => void
|
||||
disabled?: boolean;
|
||||
uninstallMod?: (mods: Mod) => void;
|
||||
uninstallMod?: (mods: BbmFullMod) => void;
|
||||
uninstallAllMods?: () => void;
|
||||
unselectAllMods?: () => void;
|
||||
openModsDropZone?: () => void;
|
||||
@@ -24,30 +24,31 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
|
||||
const [filter, setFilter] = useState("");
|
||||
const [filterEnabled, setFilterEnabled] = useState(false);
|
||||
const t = useTranslation();
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
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());
|
||||
|
||||
@@ -75,21 +76,20 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
]} />
|
||||
</span>
|
||||
|
||||
{Array.from(modsMap.keys()).map(
|
||||
key =>
|
||||
modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
|
||||
{Array.from(modsMap.keys()).map(key => modsMap.get(key).some(mod => mod.mod.name.toLowerCase().includes(filter)) && (
|
||||
<ul key={key} className="contents">
|
||||
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
|
||||
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && (
|
||||
{modsMap.get(key).map(mod => mod.mod.name.toLowerCase().includes(filter) && (
|
||||
<ModItem
|
||||
key={mod.name}
|
||||
key={mod.mod.id}
|
||||
className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer"
|
||||
mod={mod} installedVersion={installedModVersion(key, mod)}
|
||||
mod={mod}
|
||||
installedVersion={installedModVersion(key, mod)}
|
||||
isDependency={isDependency(mod)}
|
||||
isSelected={isSelected(mod)}
|
||||
onChange={val => onModChange(val, mod)}
|
||||
onWantInfo={onWantInfos}
|
||||
wantInfo={mod.name === moreInfoMod?.name}
|
||||
wantInfo={mod.mod.id === moreInfoMod?.mod.id}
|
||||
disabled={disabled}
|
||||
onUninstall={() => uninstallMod?.(mod)} />
|
||||
))}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { ReactNode, 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, BbmModVersion } 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";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png";
|
||||
import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useTranslation, useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { useInView } from "framer-motion";
|
||||
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
@@ -26,7 +25,7 @@ import { Dropzone } from "renderer/components/shared/dropzone.component";
|
||||
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
|
||||
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
|
||||
|
||||
const t = useTranslation();
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
const modsManager = useService(BsModsManagerService);
|
||||
const configService = useService(ConfigurationService);
|
||||
@@ -37,10 +36,10 @@ 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<string, Mod[]>);
|
||||
const [modsInstalled, setModsInstalled] = useState(null as Map<string, Mod[]>);
|
||||
const [modsSelected, setModsSelected] = useState([] as Mod[]);
|
||||
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
|
||||
const [modsAvailable, setModsAvailable] = useState(null as Map<BbmCategories, BbmFullMod[]>);
|
||||
const [modsInstalled, setModsInstalled] = useState(null as Map<BbmCategories, BbmFullMod[]>);
|
||||
const [modsSelected, setModsSelected] = useState([] as BbmFullMod[]);
|
||||
const [moreInfoMod, setMoreInfoMod] = useState(null as BbmFullMod);
|
||||
const [reinstallAllMods, setReinstallAllMods] = useState(false);
|
||||
const isOnline = useObservable(() => os.isOnline$);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
@@ -50,86 +49,53 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
const downloadRef = useRef(null);
|
||||
const [downloadWith, setDownloadWidth] = useState(0);
|
||||
|
||||
const modsToCategoryMap = (mods: Mod[]): Map<string, Mod[]> => {
|
||||
const modsToCategoryMap = (mods: BbmFullMod[]): Map<BbmCategories, BbmFullMod[]> => {
|
||||
if (!mods) {
|
||||
return new Map<string, Mod[]>();
|
||||
return new Map<BbmCategories, BbmFullMod[]>();
|
||||
}
|
||||
const map = new Map<string, Mod[]>();
|
||||
mods.forEach(mod => map.set(mod.category, [...(map.get(mod.category) ?? []), mod]));
|
||||
const map = new Map<BbmCategories, BbmFullMod[]>();
|
||||
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(mods => {
|
||||
if (mods.some(m => m.name === mod.name)) {
|
||||
if (mods.some(m => m.mod.id === mod.mod.id)) {
|
||||
return mods;
|
||||
}
|
||||
return [...mods, mod];
|
||||
});
|
||||
}
|
||||
|
||||
setModsSelected(mods => mods.filter(m => m.name !== mod.name));
|
||||
setModsSelected(mods => mods.filter(m => m.mod.id !== mod.mod.id));
|
||||
};
|
||||
|
||||
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) {
|
||||
return;
|
||||
if (moreInfoMod?.mod?.gitUrl) {
|
||||
linkOpener.open(moreInfoMod.mod.gitUrl);
|
||||
}
|
||||
linkOpener.open(moreInfoMod.link);
|
||||
};
|
||||
// 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 async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]> {
|
||||
// const availableMods = await this.beatModsApi.getVersionMods(version);
|
||||
// return Array.from(
|
||||
// new Map<string, Mod>(
|
||||
// availableMods.reduce((res, mod) => {
|
||||
// if (mod.required || this.isDependency(mod, mods, availableMods)) {
|
||||
// res.push([mod.name, mod]);
|
||||
// }
|
||||
// return res;
|
||||
// }, [])
|
||||
// ).values()
|
||||
// );
|
||||
// }
|
||||
const getAllDependencies = (mods: Mod[], availableMods: Mod[]): Mod[] => {
|
||||
const collectedDependencies = new Set<Mod>();
|
||||
const getAllDependencies = (mods: BbmFullMod[], availableMods: BbmFullMod[]): BbmFullMod[] => {
|
||||
const collectedDependencies = new Set<BbmFullMod>();
|
||||
const modIdsToProcess = new Set(mods.flatMap(m => m.version.dependencies));
|
||||
|
||||
const collectDependencies = (mod: Mod) => {
|
||||
if (!mod.dependencies) { return; }
|
||||
for (const dependency of mod.dependencies) {
|
||||
const dependencyMod = availableMods.find(avMod => avMod.name === dependency.name);
|
||||
if (dependencyMod && !collectedDependencies.has(dependencyMod)) {
|
||||
collectedDependencies.add(dependencyMod);
|
||||
collectDependencies(dependencyMod);
|
||||
}
|
||||
for (const currentId of modIdsToProcess) {
|
||||
const dependency = availableMods.find(m => m.version.id === currentId);
|
||||
if (dependency && !collectedDependencies.has(dependency)) {
|
||||
collectedDependencies.add(dependency);
|
||||
dependency.version.dependencies?.forEach(depId => modIdsToProcess.add(depId));
|
||||
}
|
||||
};
|
||||
|
||||
mods.forEach(collectDependencies);
|
||||
|
||||
availableMods.forEach(mod => {
|
||||
if(mod.required){
|
||||
collectedDependencies.add(mod);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(collectedDependencies);
|
||||
};
|
||||
@@ -145,18 +111,23 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
let modsToInstall = [
|
||||
...modsSelected,
|
||||
...getAllDependencies(modsSelected, Array.from(modsAvailable.values()).flat())
|
||||
]
|
||||
];
|
||||
|
||||
modsToInstall = reinstallAll ? (
|
||||
modsToInstall // If reinstalling all, we install all selected mods
|
||||
) : (
|
||||
modsToInstall.filter(mod => { // Else we only install the mods that are not installed or have a newer version
|
||||
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
|
||||
return !installedMod || lt(installedMod.version, mod.version);
|
||||
const installedMod = modsInstalled.get(mod.mod.category)?.find(installedMod => installedMod.mod.id === mod.mod.id);
|
||||
return !installedMod || lt(installedMod.version.modVersion, mod.version.modVersion);
|
||||
})
|
||||
);
|
||||
|
||||
modsToInstall = Array.from(new Set(modsToInstall)); // Remove duplicates
|
||||
// Remove duplicates, null and undefined
|
||||
const set = new Set(modsToInstall);
|
||||
set.delete(null);
|
||||
set.delete(undefined);
|
||||
|
||||
modsToInstall = Array.from(set); // Remove duplicates
|
||||
|
||||
if (!modsToInstall.length) {
|
||||
notification.notifyInfo({ title: "pages.version-viewer.mods.notifications.all-mods-already-installed.title", desc: "pages.version-viewer.mods.notifications.all-mods-already-installed.description" });
|
||||
@@ -172,10 +143,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
|
||||
const importMods = (files: string[]): void => {
|
||||
setModsDropZoneOpen(false);
|
||||
modsManager.importMods(files, version);
|
||||
modsManager.importMods(files, version).then(() => {
|
||||
loadMods();
|
||||
});
|
||||
};
|
||||
|
||||
const uninstallMod = (mod: Mod): void => {
|
||||
const uninstallMod = (mod: BbmFullMod): void => {
|
||||
setUninstalling(() => true);
|
||||
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
|
||||
loadMods();
|
||||
@@ -206,17 +179,23 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const promise = async () => {
|
||||
const promise = async (): Promise<[BbmFullMod[], BbmModVersion[]]> => {
|
||||
const available = await lastValueFrom(modsManager.getAvailableMods(version));
|
||||
const installed = await lastValueFrom(modsManager.getInstalledMods(version));
|
||||
return [available, installed];
|
||||
}
|
||||
|
||||
return promise().then(([available, installed]) => {
|
||||
const defaultMods = installed?.length ? [] : configService.get<string[]>("default_mods" as DefaultConfigKey);
|
||||
const defaultMods = installed?.length ? [] : available.filter(m => m.mod.category === BbmCategories.Core || m.mod.category === BbmCategories.Essential);
|
||||
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;
|
||||
}).filter(mod => mod);
|
||||
|
||||
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installedMods.some(i => m.mod.id === i.mod.id)));
|
||||
setModsInstalled(modsToCategoryMap(installedMods));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const defaultConfiguration: {
|
||||
"second-color": "#ff4444",
|
||||
theme: "os",
|
||||
language: window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
|
||||
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP", "ko-KR"],
|
||||
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "it-IT", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP", "ko-KR"],
|
||||
default_mods: ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"],
|
||||
"default-shared-folders": [
|
||||
window.electron.path.join("Beat Saber_Data", "CustomLevels"),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Observable, BehaviorSubject, throwError, of, lastValueFrom } from "rxjs";
|
||||
import { catchError, map, tap } from "rxjs/operators";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { Mod } from "shared/models/mods";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -32,15 +32,15 @@ export class BsModsManagerService {
|
||||
this.notifications = NotificationService.getInstance();
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
|
||||
public getAvailableMods(version: BSVersion): Observable<BbmFullMod[]> {
|
||||
return this.ipcService.sendV2("bs-mods.get-available-mods", version);
|
||||
}
|
||||
|
||||
public getInstalledMods(version: BSVersion): Observable<Mod[]> {
|
||||
public getInstalledMods(version: BSVersion): Observable<BbmModVersion[]> {
|
||||
return this.ipcService.sendV2("bs-mods.get-installed-mods", version);
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
public installMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
|
||||
|
||||
if (!this.progressBar.require()) {
|
||||
return throwError(() => new Error("Action already in progress"));
|
||||
@@ -72,7 +72,7 @@ export class BsModsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public uninstallMod(mod: Mod, version: BSVersion): Observable<Progression> {
|
||||
public uninstallMod(mod: BbmFullMod, version: BSVersion): Observable<Progression> {
|
||||
if (!this.progressBar.require()) {
|
||||
return throwError(() => new Error("Action already in progress"));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { coerce, lt, valid } from "semver";
|
||||
import { coerce, lt, gt, valid } from "semver";
|
||||
import { tryit } from "./error.helpers";
|
||||
|
||||
export function safeLt(a: string, b: string): boolean {
|
||||
@@ -6,3 +6,8 @@ export function safeLt(a: string, b: string): boolean {
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
export function safeGt(a: string, b: string): boolean {
|
||||
const { result } = tryit(() => gt(valid(coerce(a)), valid(coerce(b))));
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { DepotDownloaderEvent } from "../bs-version-download/depot-downloader.mo
|
||||
import { MSGetQuery, MSModel, MSModelType } from "../models/model-saber.model";
|
||||
import { ModelDownload } from "renderer/services/models-management/models-downloader.service";
|
||||
import { BsmLocalModel } from "../models/bsm-local-model.interface";
|
||||
import { Mod } from "../mods";
|
||||
import { BPList, DownloadPlaylistProgressionData } from "../playlists/playlist.interface";
|
||||
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
|
||||
import { FileFilter, OpenDialogOptions, OpenDialogReturnValue } from "electron";
|
||||
@@ -20,7 +19,7 @@ import { Supporter } from "../supporters";
|
||||
import { AppWindow } from "../window-manager/app-window.model";
|
||||
import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.models";
|
||||
import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpcRequest } from "main/services/static-configuration.service";
|
||||
import { ExternalMod } from "../mods/mod.interface";
|
||||
import { BbmFullMod, BbmModVersion, ExternalMod } from "../mods/mod.interface";
|
||||
import { OculusDownloadInfo } from "main/services/bs-version-download/bs-oculus-downloader.service";
|
||||
|
||||
export type IpcReplier<T> = (data: Observable<T>) => void;
|
||||
@@ -83,11 +82,11 @@ export interface IpcChannelMapping {
|
||||
"delete-models": { request: BsmLocalModel[], response: Progression<BsmLocalModel[]> };
|
||||
|
||||
/* ** bs-mods-ipcs ** */
|
||||
"bs-mods.get-available-mods": { request: BSVersion, response: Mod[] };
|
||||
"bs-mods.get-installed-mods": { request: BSVersion, response: Mod[] };
|
||||
"bs-mods.get-available-mods": { request: BSVersion, response: BbmFullMod[] };
|
||||
"bs-mods.get-installed-mods": { request: BSVersion, response: BbmModVersion[] };
|
||||
"bs-mods.import-mods": { request: { paths: string[]; version: BSVersion; }, response: Progression<ExternalMod> };
|
||||
"bs-mods.install-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"bs-mods.uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"bs-mods.install-mods": { request: { mods: BbmFullMod[]; version: BSVersion }, response: Progression };
|
||||
"bs-mods.uninstall-mods": { request: { mods: BbmFullMod[]; version: BSVersion }, response: Progression };
|
||||
"bs-mods.uninstall-all-mods": { request: BSVersion, response: Progression };
|
||||
|
||||
/* ** bs-playlist-ipcs ** */
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { ModInstallProgression, InstallModsResult, UninstallModsResult } from "./mod-ipc.model";
|
||||
export { Mod, DownloadLink, DownloadLinkType, FileHashes, ModAuthor } from "./mod.interface";
|
||||
|
||||
@@ -1,42 +1,101 @@
|
||||
export interface Mod {
|
||||
_id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
gameVersion: string;
|
||||
authorId: string;
|
||||
uploadedDate: string;
|
||||
updatedDate: string;
|
||||
author: ModAuthor;
|
||||
description: string;
|
||||
link: string;
|
||||
category: string;
|
||||
downloads: DownloadLink[];
|
||||
required: boolean;
|
||||
dependencies: Mod[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ModAuthor {
|
||||
_id: string;
|
||||
username: string;
|
||||
lastLogin: string;
|
||||
}
|
||||
|
||||
export interface DownloadLink {
|
||||
type: DownloadLinkType;
|
||||
url: string;
|
||||
hashMd5: FileHashes[];
|
||||
}
|
||||
|
||||
export type DownloadLinkType = "universal" | "steam" | "oculus";
|
||||
|
||||
export interface FileHashes {
|
||||
hash: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
// Any mods that are not supported in beatmods
|
||||
export interface ExternalMod {
|
||||
name: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
// BBM Mods
|
||||
|
||||
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;
|
||||
author: BbmUserAPIResponse;
|
||||
modVersion: string;
|
||||
platform: BbmPlatform;
|
||||
zipHash: string;
|
||||
status: BbmStatus;
|
||||
dependencies: number[];
|
||||
contentHashes: BbmContentHash[];
|
||||
supportedGameVersions: BbmGameVersion[];
|
||||
downloadCount: number;
|
||||
lastApprovedById?: number;
|
||||
lastUpdatedById?: number;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface BbmContentHash {
|
||||
path: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export enum BbmStatus {
|
||||
Private = "private",
|
||||
Removed = "removed",
|
||||
Unverified = "unverified",
|
||||
Verified = "verified",
|
||||
}
|
||||
|
||||
export enum BbmPlatform {
|
||||
SteamPC = `steampc`,
|
||||
OculusPC = `oculuspc`,
|
||||
UniversalPC = `universalpc`,
|
||||
UniversalQuest = `universalquest`,
|
||||
}
|
||||
|
||||
export interface BbmGameVersion {
|
||||
readonly id: number;
|
||||
gameName: "BeatSaber";
|
||||
version: string; // semver
|
||||
defaultVersion: boolean;
|
||||
}
|
||||
|
||||
export enum BbmCategories {
|
||||
Core = "core",
|
||||
Essential = "essential",
|
||||
Library = "library",
|
||||
Cosmetic = "cosmetic",
|
||||
PracticeTraining = "practice",
|
||||
Gameplay = "gameplay",
|
||||
StreamTools = "streamtools",
|
||||
UIEnhancements = "ui",
|
||||
Lighting = "lighting",
|
||||
TweaksTools = "tweaks",
|
||||
Multiplayer = "multiplayer",
|
||||
TextChanges = "text",
|
||||
Editor = "editor",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export interface BbmUserAPIResponse {
|
||||
id: number;
|
||||
username: string;
|
||||
githubId: string;
|
||||
sponsorUrl: string;
|
||||
displayName: string;
|
||||
bio: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user