From 555a9bc98e17abf8c3b085da3db6901d62a6e163 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sat, 4 Nov 2023 18:09:55 +0100 Subject: [PATCH] [feature-274] Add backup inforamtion modal before launch oculus version --- src/main/helpers/fs.helpers.ts | 5 ++ .../bs-launcher/oculus-launcher.service.ts | 62 +++++++++++------- src/main/services/bs-local-version.service.ts | 2 +- src/main/services/oculus.service.ts | 4 +- .../original-oculus-version-backup.modal.tsx | 52 +++++++++++++++ .../bs-version-item.component.tsx | 5 +- .../slides/launch/launch-slide.component.tsx | 5 +- src/renderer/services/bs-launcher.service.ts | 63 ++++++++++++++----- 8 files changed, 151 insertions(+), 47 deletions(-) create mode 100644 src/renderer/components/modal/modal-types/original-oculus-version-backup.modal.tsx diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index f0a9b63b..743e60e9 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -217,6 +217,11 @@ export async function ensurePathNotAlreadyExist(path: string): Promise { return destPath; } +export async function isJunction(path: string): Promise{ + const [stats, lstats] = await Promise.all([stat(path), lstat(path)]); + return lstats.isSymbolicLink() && stats.isDirectory(); +} + export interface Progression { total: number; current: number; diff --git a/src/main/services/bs-launcher/oculus-launcher.service.ts b/src/main/services/bs-launcher/oculus-launcher.service.ts index 5bb024b4..fd0a2875 100644 --- a/src/main/services/bs-launcher/oculus-launcher.service.ts +++ b/src/main/services/bs-launcher/oculus-launcher.service.ts @@ -1,14 +1,15 @@ import { Observable, ReplaySubject, catchError, lastValueFrom, of, take, timeout } from "rxjs"; import { StoreLauncherInterface } from "./store-launcher.interface"; -import { BSLaunchError, BSLaunchErrorData, BSLaunchEventData, LaunchOption } from "../../../shared/models/bs-launch"; +import { BSLaunchError, BSLaunchErrorData, BSLaunchEvent, BSLaunchEventData, LaunchOption } from "../../../shared/models/bs-launch"; import { OculusService } from "../oculus.service"; import { BS_EXECUTABLE, OCULUS_BS_BACKUP_DIR, OCULUS_BS_DIR } from "../../constants"; import path from "path"; import log from "electron-log"; import { sToMs } from "../../../shared/helpers/time.helpers"; -import { pathExists, readdir, rename, stat, symlink, unlink } from "fs-extra"; +import { lstat, pathExists, readdir, rename, stat, symlink, unlink } from "fs-extra"; import { AbstractLauncherService } from "./abstract-launcher.service"; import { taskRunning } from "../../helpers/os.helpers"; +import { isJunction } from "../../helpers/fs.helpers"; export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface { @@ -48,16 +49,11 @@ export class OculusLauncherService extends AbstractLauncherService implements St } const libContents = await readdir(oculusLibPath, { withFileTypes: true}); - const symlinks = libContents.filter(dirent => dirent.isSymbolicLink()); - - const dirSymlinks = (await Promise.all(symlinks.map(async symlink => { - const symlinkPath = path.join(oculusLibPath, symlink.name); - const symlinkStats = await stat(symlinkPath); - if(!symlinkStats.isDirectory()){ return null; } - return symlink; + const junctions = (await Promise.all(libContents.map(async dirent => { + return (await isJunction(path.join(oculusLibPath, dirent.name))) ? dirent : null; }))).filter(Boolean); - const bsSymlinks = dirSymlinks.filter(dirent => dirent.name.startsWith(OCULUS_BS_DIR)); + const bsSymlinks = junctions.filter(dirent => dirent.name.startsWith(OCULUS_BS_DIR)); // get only symlinks created by BSM (with metadata.config) const bsmSymlinks = (await Promise.all(bsSymlinks.map(async symlink => { @@ -90,6 +86,31 @@ export class OculusLauncherService extends AbstractLauncherService implements St // TODO : Convert all errors to CustomError public launch(launchOptions: LaunchOption): Observable { + + const prepareOriginalVersion: () => Promise = async () => { + await this.restoreOriginalBeatSaber(); + return this.oculus.getGameFolder(OCULUS_BS_DIR); + } + + const prepareDowngradedVersion: () => Promise = async () => { + + const oculusLib = await lastValueFrom(this.oculusLib$.pipe(take(1), timeout(sToMs(30)), catchError(() => of(null)))); + + if(!oculusLib){ + throw new Error("No Oculus library found"); + } + + // Backup original Beat Saber folder + await this.backupOriginalBeatSaber(); + + // Create symlink in the oculus library from the BSM BS version + const symlinkTarget = await this.localVersions.getInstalledVersionPath(launchOptions.version); + const symlinkPath = path.join(oculusLib, OCULUS_BS_DIR); + await symlink(symlinkTarget, symlinkPath, "junction"); + + return symlinkPath; + } + return new Observable(obs => { (async () => { @@ -98,26 +119,19 @@ export class OculusLauncherService extends AbstractLauncherService implements St if(bsRunning){ throw ({type: BSLaunchError.BS_ALREADY_RUNNING, data: bsRunning}) as BSLaunchErrorData; } - - const oculusLib = await lastValueFrom(this.oculusLib$.pipe(take(1), timeout(sToMs(30)), catchError(() => of(null)))); - - if(!oculusLib){ - throw new Error("No Oculus library found"); - } - + // Remove previously symlinks created by BSM await this.deleteBsSymlinks().catch(log.error); - // Backup original Beat Saber folder - await this.backupOriginalBeatSaber(); + const bsPath = await (launchOptions.version.oculus ? prepareOriginalVersion() : prepareDowngradedVersion()); - // Create symlink in the oculus library from the BSM BS version - const symlinkTarget = await this.localVersions.getInstalledVersionPath(launchOptions.version); - const symlinkPath = path.join(oculusLib, OCULUS_BS_DIR); - await symlink(symlinkTarget, symlinkPath, "junction"); + if(!bsPath){ + throw ({type: BSLaunchError.BS_NOT_FOUND}) as BSLaunchErrorData; + } // Launch Beat Saber - const exePath = path.join(symlinkPath, "Beat Saber.exe"); + const exePath = path.join(bsPath, "Beat Saber.exe"); + obs.next({type: BSLaunchEvent.BS_LAUNCHING}); return this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions)).catch(err => { throw ({type: BSLaunchError.BS_EXIT_ERROR, data: err}) as BSLaunchErrorData; }); diff --git a/src/main/services/bs-local-version.service.ts b/src/main/services/bs-local-version.service.ts index e8b1ff23..c15d646f 100644 --- a/src/main/services/bs-local-version.service.ts +++ b/src/main/services/bs-local-version.service.ts @@ -181,7 +181,7 @@ export class BSLocalVersionService { */ public async getVersionPath(version: BSVersion): Promise{ if(version.steam){ return this.steamService.getGameFolder(BS_APP_ID, "Beat Saber") } - if(version.oculus){ return this.oculusService.getGameFolder(OCULUS_BS_DIR); } + if(version.oculus){ return this.oculusService.tryGetGameFolder([OCULUS_BS_DIR, OCULUS_BS_BACKUP_DIR]); } return path.join( await this.installLocationService.versionsDirectory(), diff --git a/src/main/services/oculus.service.ts b/src/main/services/oculus.service.ts index 4e22f882..c6a9ac7b 100644 --- a/src/main/services/oculus.service.ts +++ b/src/main/services/oculus.service.ts @@ -1,6 +1,6 @@ import regedit from "regedit"; import path from "path"; -import { pathExist } from "../helpers/fs.helpers"; +import { isJunction, pathExist } from "../helpers/fs.helpers"; import log from "electron-log"; export class OculusService { @@ -67,7 +67,7 @@ export class OculusService { for (const { path: libPath } of libsFolders) { const gameFullPath = path.join(libPath, rootLibDir, gameFolder); if (await pathExist(gameFullPath)) { - return gameFullPath; + return (await isJunction(gameFullPath)) ? null : gameFullPath; } } diff --git a/src/renderer/components/modal/modal-types/original-oculus-version-backup.modal.tsx b/src/renderer/components/modal/modal-types/original-oculus-version-backup.modal.tsx new file mode 100644 index 00000000..455e4b65 --- /dev/null +++ b/src/renderer/components/modal/modal-types/original-oculus-version-backup.modal.tsx @@ -0,0 +1,52 @@ +import { ModalComponent, ModalExitCode } from "renderer/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 { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; +import { useState } from "react"; + +export const OriginalOculusVersionBackupModal: ModalComponent = ({ resolver }) => { + + const [dontShowAgain, setDontShowAgain] = useState(false); + + const submit = () => { + resolver({ exitCode: ModalExitCode.COMPLETED, data: dontShowAgain }); + } + + // TODO : Translate + + return ( +
+

Attention

+ + +

Pour lancer cette version, le dossier d'installation de Beat Saber se trouvant dans votre librairie Oculus va être renommé.

+

Au besoin, vous pourrez le restaurer depuis BSManager en vous rendant dans les options de la version originale et en cliquant sur "Restaurer le dossier"

+

Astuce : Vous pourrez lancer cette version directement depuis Oculus, tant que la version originale n'est pas restaurée.

+ +
+ + Ne plus me rappler +
+ +
+ { + resolver({ exitCode: ModalExitCode.CANCELED }); + }} + withBar={false} + text="misc.cancel" + /> + +
+ + ) +} diff --git a/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx index 3d0d802a..2a9eb236 100644 --- a/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx +++ b/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx @@ -1,7 +1,7 @@ import { BSVersion } from "shared/bs-version.interface"; import { Link, useLocation } from "react-router-dom"; import { useState } from "react"; -import { distinctUntilChanged, map, of, Subscription, switchMap } from "rxjs"; +import { distinctUntilChanged, lastValueFrom, map, of, Subscription, switchMap } from "rxjs"; import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service"; import { ConfigurationService } from "renderer/services/configuration.service"; import { BSUninstallerService } from "renderer/services/bs-uninstaller.service"; @@ -56,12 +56,13 @@ export function BsVersionItem(props: { version: BSVersion }) { }; const handleDoubleClick = () => { - launcherService.launch({ + const launch$ = launcherService.launch({ version: state, oculus: !!configService.get(LaunchMods.OCULUS_MOD), desktop: !!configService.get(LaunchMods.DESKTOP_MOD), debug: !!configService.get(LaunchMods.DEBUG_MOD), }); + return lastValueFrom(launch$).catch(() => {}); }; const cancel = () => { diff --git a/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx index cf3e3d1d..5475687a 100644 --- a/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx +++ b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx @@ -11,6 +11,7 @@ import BSLogo from "../../../../../../assets/images/apngs/bs-logo.png"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { useService } from "renderer/hooks/use-service.hook"; import { BsStore } from "shared/models/bs-store.enum"; +import { lastValueFrom } from "rxjs"; type Props = { version: BSVersion }; @@ -58,13 +59,15 @@ export function LaunchSlide({ version }: Props) { .filter(arg => arg.length > 0) : undefined; - bsLauncherService.launch({ + const launch$ = bsLauncherService.launch({ version, oculus: version.oculus ? false : oculusMode, desktop: desktopMode, debug: debugMode, additionalArgs }); + + return lastValueFrom(launch$).catch(() => {}); }; return ( diff --git a/src/renderer/services/bs-launcher.service.ts b/src/renderer/services/bs-launcher.service.ts index 602548f1..9aa09461 100644 --- a/src/renderer/services/bs-launcher.service.ts +++ b/src/renderer/services/bs-launcher.service.ts @@ -2,9 +2,12 @@ import { LaunchOption, BSLaunchEvent, BSLaunchWarning, BSLaunchEventData, BSLaun import { BSVersion } from 'shared/bs-version.interface'; import { IpcService } from "./ipc.service"; import { NotificationService } from "./notification.service"; -import { BehaviorSubject, Observable, filter, lastValueFrom } from "rxjs"; +import { BehaviorSubject, Observable, lastValueFrom, tap } from "rxjs"; import { ConfigurationService } from "./configuration.service"; import { ThemeService } from "./theme.service"; +import { BsStore } from "shared/models/bs-store.enum"; +import { ModalExitCode, ModalService } from "./modale.service"; +import { OriginalOculusVersionBackupModal } from "renderer/components/modal/modal-types/original-oculus-version-backup.modal"; export class BSLauncherService { private static instance: BSLauncherService; @@ -13,6 +16,7 @@ export class BSLauncherService { private readonly notificationService: NotificationService; private readonly config: ConfigurationService; private readonly theme: ThemeService; + private readonly modals: ModalService; public readonly versionRunning$: BehaviorSubject = new BehaviorSubject(null); @@ -26,6 +30,15 @@ export class BSLauncherService { this.notificationService = NotificationService.getInstance(); this.config = ConfigurationService.getInstance(); this.theme = ThemeService.getInstance(); + this.modals = ModalService.getInstance(); + } + + private notRewindBackupOculus(): boolean{ + return this.config.get("not-rewind-backup-oculus"); + } + + private setNotRewindBackupOculus(value: boolean): void{ + this.config.set("not-rewind-backup-oculus", value); } public getLaunchOptions(version: BSVersion): LaunchOption{ @@ -37,35 +50,51 @@ export class BSLauncherService { additionalArgs: (this.config.get("additionnal-args") || "").split(";").map(arg => arg.trim()).filter(arg => arg.length > 0) } } - - public doLaunch(launchOptions: LaunchOption): Observable{ - return this.ipcService.sendV2("bs-launch.launch", {args: launchOptions}); - } - public launch(launchOptions: LaunchOption): Observable { - const launchState$ = this.doLaunch(launchOptions); + private handleLaunchEvents(events$: Observable): Observable{ + const eventToFilter = [...Object.values(BSLaunchWarning), BSLaunchEvent.STEAM_LAUNCHED] - this.versionRunning$.next(launchOptions.version); - - launchState$.pipe(filter(event => { - const eventToFilter = [...Object.values(BSLaunchWarning), BSLaunchEvent.STEAM_LAUNCHED] - return !eventToFilter.includes(event.type); - })).subscribe({ + return events$.pipe(tap({ next: event => { + if(eventToFilter.includes(event.type)){ return; } this.notificationService.notifySuccess({title: `notifications.bs-launch.success.titles.${event.type}`, desc: `notifications.bs-launch.success.msg.${event.type}`}); }, - error: (err: BSLaunchErrorData) => { + error: (err: BSLaunchErrorData) => { // TODO : Convert all errors to CustomError if(!Object.values(BSLaunchError).includes(err.type)){ this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.UNKNOWN_ERROR", desc: "notifications.bs-launch.errors.msg.UNKNOWN_ERROR"}); } else { this.notificationService.notifyError({title: `notifications.bs-launch.errors.titles.${err.type}`, desc: `notifications.bs-launch.errors.msg.${err.type}`}) } } - }).add(() => { - this.versionRunning$.next(null); + })) + } + + public doLaunch(launchOptions: LaunchOption): Observable{ + return this.ipcService.sendV2("bs-launch.launch", {args: launchOptions}); + } + + public launch(launchOptions: LaunchOption): Observable { + + return new Observable(obs => { + (async () => { + + if(launchOptions.version.metadata?.store === BsStore.OCULUS && !this.notRewindBackupOculus()){ + const { exitCode, data: notRewind } = await this.modals.openModal(OriginalOculusVersionBackupModal); + if(exitCode !== ModalExitCode.COMPLETED){ return; } + this.setNotRewindBackupOculus(notRewind); + } + + const launch$ = this.handleLaunchEvents(this.doLaunch(launchOptions)); + + await lastValueFrom(launch$); + + })().then(() => { + obs.complete(); + }).catch(err => { + obs.error(err); + }) }); - return launchState$; } public createLaunchShortcut(launchOptions: LaunchOption): Observable{