diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index 066256e4..a53e1a86 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -335,12 +335,17 @@ "bs-launch": { "success": { "titles": { - "launching": "Starte...🚀" + "BS_LAUNCHING": "Starte...🚀", + "STEAM_LAUNCHING": "Steam startet!" + }, + "msg": { + "BS_LAUNCHING": "TODO", + "STEAM_LAUNCHING": "Beat Saber wird automatisch nach Steam gestartet." } }, "errors": { "titles": { - "UNABLE_TO_LAUNCH": "Start nicht möglich", + "UNKNOWN_ERROR": "Start nicht möglich", "STEAM_NOT_RUNNING": "Steam läuft nicht", "OCULUS_NOT_RUNNING": "Oculus läuft nicht", "BS_ALREADY_RUNNING": "BeatSaber läuft bereits", diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 87ead7b0..8c069767 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -335,12 +335,17 @@ "bs-launch": { "success": { "titles": { - "launching": "Launching...🚀" + "BS_LAUNCHING": "Launching...🚀", + "STEAM_LAUNCHING": "Steam is launching!" + }, + "msg": { + "BS_LAUNCHING": "TODO", + "STEAM_LAUNCHING": "Beat Saber will automatically launch after Steam." } }, "errors": { "titles": { - "UNABLE_TO_LAUNCH": "Unable to launch", + "UNKNOWN_ERROR": "Unable to launch", "STEAM_NOT_RUNNING": "Steam is not running", "OCULUS_NOT_RUNNING": "Oculus is not running", "BS_ALREADY_RUNNING": "BeatSaber already running", diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 55a6dadf..90d23ea0 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -334,12 +334,17 @@ "bs-launch": { "success": { "titles": { - "launching": "Lanzamiento...🚀" + "BS_LAUNCHING": "Lanzamiento...🚀", + "STEAM_LAUNCHING": "¡Steam se está iniciando!" + }, + "msg": { + "BS_LAUNCHING": "TODO", + "STEAM_LAUNCHING": "Beat Saber se lanzará automáticamente después de Steam." } }, "errors": { "titles": { - "UNABLE_TO_LAUNCH": "No se puede lanzar", + "UNKNOWN_ERROR": "No se puede lanzar", "STEAM_NOT_RUNNING": "Steam no funciona", "OCULUS_NOT_RUNNING": "Oculus no funciona", "BS_ALREADY_RUNNING": "BeatSaber ya está en marcha", diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 8e7d4305..c5644dcf 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -334,12 +334,17 @@ "bs-launch": { "success": { "titles": { - "launching": "Lancement...🚀" + "BS_LAUNCHING": "Lancement...🚀", + "STEAM_LAUNCHING": "Steam se lance !" + }, + "msg": { + "BS_LAUNCHING": "TODO", + "STEAM_LAUNCHING": "Beat Saber se lancera automatiquement après Steam" } }, "errors": { "titles": { - "UNABLE_TO_LAUNCH": "Lancement impossible", + "UNKNOWN_ERROR": "Lancement impossible", "STEAM_NOT_RUNNING": "Steam n'est pas lancé", "OCULUS_NOT_RUNNING": "Oculus n'est pas lancé", "BS_ALREADY_RUNNING": "Beat Saber est déjà lancé", diff --git a/src/main/ipcs/bs-launcher-ipcs.ts b/src/main/ipcs/bs-launcher-ipcs.ts index 53ff080a..5efddc67 100644 --- a/src/main/ipcs/bs-launcher-ipcs.ts +++ b/src/main/ipcs/bs-launcher-ipcs.ts @@ -1,17 +1,25 @@ import { ipcMain } from 'electron'; import { UtilsService } from '../services/utils.service' -import { LauchOption } from "shared/models/bs-launch"; +import { LaunchOption } from "shared/models/bs-launch"; import { BSLauncherService } from "../services/bs-launcher.service" import { IpcRequest } from 'shared/models/ipc'; import { BsmException } from 'shared/models/bsm-exception.model'; +import { IpcService } from '../services/ipc.service'; -ipcMain.on('bs-launch.launch', (event, request: IpcRequest) => { - const launcherService = BSLauncherService.getInstance(); - const utilsService = UtilsService.getInstance(); +// ipcMain.on('bs-launch.launch', (event, request: IpcRequest) => { +// const launcherService = BSLauncherService.getInstance(); +// const utilsService = UtilsService.getInstance(); - launcherService.launch(request.args).then(res => { - utilsService.ipcSend(request.responceChannel, {success: true, data: res}); - }).catch((err: BsmException) => { - utilsService.ipcSend(request.responceChannel, {success: false, error: err}); - }) +// launcherService.launch(request.args).then(res => { +// utilsService.ipcSend(request.responceChannel, {success: true, data: res}); +// }).catch((err: BsmException) => { +// utilsService.ipcSend(request.responceChannel, {success: false, error: err}); +// }) +// }); + +const ipc = IpcService.getInstance(); + +ipc.on('bs-launch.launch', async (req: IpcRequest, reply) => { + const bsLauncher = BSLauncherService.getInstance(); + reply(bsLauncher.launchV2(req.args)); }); diff --git a/src/main/services/bs-launcher.service.ts b/src/main/services/bs-launcher.service.ts index 02eae74d..e41b3662 100644 --- a/src/main/services/bs-launcher.service.ts +++ b/src/main/services/bs-launcher.service.ts @@ -1,15 +1,15 @@ import path from "path"; -import { LaunchResult, LauchOption } from "shared/models/bs-launch"; +import { LaunchResult, LaunchOption, BSLaunchEvent, BSLaunchErrorEvent, BSLaunchErrorType, BSLaunchEventType } from "../../shared/models/bs-launch"; import { UtilsService } from "./utils.service"; import { BS_EXECUTABLE, BS_APP_ID, STEAMVR_APP_ID } from "../constants"; -import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio, spawn } from "child_process"; import { SteamService } from "./steam.service"; import { BSLocalVersionService } from "./bs-local-version.service"; import { OculusService } from "./oculus.service"; import { pathExist } from "../helpers/fs.helpers"; import { rename } from "fs/promises"; import log from "electron-log"; -import { timer } from "rxjs"; +import { Observable, lastValueFrom, timer } from "rxjs"; import { NotificationService } from "./notification.service"; import { NotificationType } from "../../shared/models/notification/notification.model"; @@ -60,7 +60,7 @@ export class BSLauncherService{ } // TODO : Rework with shortcuts implementation - public async launch(launchOptions: LauchOption): Promise{ + public async launch(launchOptions: LaunchOption): Promise{ if(this.isBsRunning() === true){ return "BS_ALREADY_RUNNING" } if(launchOptions.version.oculus && this.oculusService.oculusRunning() === false){ return "OCULUS_NOT_RUNNING" } @@ -121,4 +121,97 @@ export class BSLauncherService{ return "LAUNCHED"; } + private buildBsLaunchArgs(launchOptions: LaunchOption){ + let launchArgs = []; + + if(!launchOptions.version.steam && !launchOptions.version.oculus){ launchArgs.push("--no-yeet"); } + if(launchOptions.oculus){ launchArgs.push("-vrmode oculus"); } + if(launchOptions.desktop){ launchArgs.push("fpfc"); } + if(launchOptions.debug){ launchArgs.push("--verbose"); } + if(launchOptions.additionalArgs){ launchArgs.push(...launchOptions.additionalArgs); } + + return Array.from(new Set(launchArgs).values()); + } + + private launchBSProcess(bsExePath: string, args: string[], debug = false): Promise{ + + if(this.bsProcess?.connected){ + return Promise.reject("Beat Saber process already running"); + } + + const spawnOptions: SpawnOptionsWithoutStdio = { shell: true, cwd: path.dirname(bsExePath), env: {...process.env, "SteamAppId": BS_APP_ID} }; + + if(debug){ + spawnOptions.detached = true; + spawnOptions.windowsVerbatimArguments = true; + } + + this.bsProcess = spawn(`\"${bsExePath}\"`, args, spawnOptions); + + return new Promise((resolve, reject) => { + this.bsProcess.on('error', e => { log.error(e); reject(e); }); + this.bsProcess.once('exit', code => { + if(code !== 0){ + log.error(`Beat Saber process exited with code ${code}`); + } + resolve(); + }); + }); + + } + + public launchV2(launchOptions: LaunchOption): Observable{ + return new Observable(obs => {(async () => { + + if(this.isBsRunning()){ + return obs.error({type: BSLaunchErrorType.BS_ALREADY_RUNNING} as BSLaunchErrorEvent); + } + + if(launchOptions.version.oculus && this.oculusService.oculusRunning() === false){ + return obs.error({type: BSLaunchErrorType.OCULUS_NOT_RUNNING} as BSLaunchErrorEvent); + } + + const bsFolderPath = await this.localVersionService.getVersionPath(launchOptions.version); + const exePath = path.join(bsFolderPath, BS_EXECUTABLE); + + if(!(await pathExist(exePath))){ + return obs.error({type: BSLaunchErrorType.BS_NOT_FOUND} as BSLaunchErrorEvent); + } + + // Open Steam if not running + if(!launchOptions.version.oculus && !(await this.steamService.steamRunning())){ + obs.next({type: BSLaunchEventType.STEAM_LAUNCHING}); + await this.steamService.openSteam().catch(log.error); + } + + // Backup SteamVR when desktop mode is enabled + if(!launchOptions.version.oculus && launchOptions.desktop){ + await this.backupSteamVR().catch(e => { + log.error("ERR_BACKUP_STEAM_VR", e); + this.restoreSteamVR(); + }); + await lastValueFrom(timer(2_000)); + } else if(!launchOptions.version.oculus){ + await this.restoreSteamVR(); + } + + const launchArgs = this.buildBsLaunchArgs(launchOptions); + + obs.next({type: BSLaunchEventType.BS_LAUNCHING}); + + await this.launchBSProcess(exePath, launchArgs, launchOptions.debug).catch(() => { + obs.error({type: BSLaunchErrorType.BS_EXIT_ERROR} as BSLaunchErrorEvent); + }).finally(() => { + if(!launchOptions.desktop || launchOptions.version.oculus){ return; } + this.restoreSteamVR().catch(e => log.error("ERR_RESTORE_STEAM_VR", e)); + }); + + })().then(() => { + obs.complete() + }).catch(err => { + obs.error({type: BSLaunchErrorType.UNKNOWN_ERROR, data: err} as BSLaunchErrorEvent); + })}); + } + + } \ No newline at end of file diff --git a/src/main/services/steam.service.ts b/src/main/services/steam.service.ts index c7e4ce38..19c3f3ef 100644 --- a/src/main/services/steam.service.ts +++ b/src/main/services/steam.service.ts @@ -9,21 +9,21 @@ import log from "electron-log"; export class SteamService{ - private static instance: SteamService; + private static instance: SteamService; - private readonly utils: UtilsService = UtilsService.getInstance(); + private readonly utils: UtilsService = UtilsService.getInstance(); - private steamPath: string = ''; + private steamPath: string = ''; - private constructor(){ - const vbsDirectory = path.join(this.utils.getAssetsScriptsPath(), "node-regedit", "vbs"); - regedit.setExternalVBSLocation(vbsDirectory); - } + private constructor(){ + const vbsDirectory = path.join(this.utils.getAssetsScriptsPath(), "node-regedit", "vbs"); + regedit.setExternalVBSLocation(vbsDirectory); + } - public static getInstance(){ - if(!SteamService.instance){ SteamService.instance = new SteamService(); } - return SteamService.instance; - } + public static getInstance(){ + if(!SteamService.instance){ SteamService.instance = new SteamService(); } + return SteamService.instance; + } public async getActiveUser(): Promise{ const res = await regedit.promisified.list(["HKCU\\Software\\Valve\\Steam\\ActiveProcess"]); @@ -40,47 +40,49 @@ export class SteamService{ .catch(e => {log.error(e); throw e}) } - public async getSteamPath(): Promise{ + public async getSteamPath(): Promise{ - if(!!this.steamPath){ return this.steamPath; } + if(!!this.steamPath){ return this.steamPath; } - const [win32Res, win64Res] = await Promise.all([ - regedit.promisified.list(['HKLM\\SOFTWARE\\Valve\\Steam']), - regedit.promisified.list(['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam']) - ]); + const [win32Res, win64Res] = await Promise.all([ + regedit.promisified.list(['HKLM\\SOFTWARE\\Valve\\Steam']), + regedit.promisified.list(['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam']) + ]); - const [win32, win64] = [win32Res["HKLM\\SOFTWARE\\Valve\\Steam"], win64Res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]]; + const [win32, win64] = [win32Res["HKLM\\SOFTWARE\\Valve\\Steam"], win64Res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]]; - let res = ''; - if(win64.exists && win64?.values?.InstallPath?.value){ res = win64.values.InstallPath.value as string; } - else if(win32.exists && win32?.values?.InstallPath?.value){ res = win32.values.InstallPath.value as string; } - this.steamPath = res; - return this.steamPath; - } + let res = ''; + + if(win64.exists && win64?.values?.InstallPath?.value){ res = win64.values.InstallPath.value as string; } + else if(win32.exists && win32?.values?.InstallPath?.value){ res = win32.values.InstallPath.value as string; } + + this.steamPath = res; + return this.steamPath; + } - public async getGameFolder(gameId: string, gameFolder?: string): Promise{ - try{ - const steamPath = await this.getSteamPath(); + public async getGameFolder(gameId: string, gameFolder?: string): Promise{ + try{ + const steamPath = await this.getSteamPath(); - let libraryFolders: any = path.join(steamPath, 'steamapps', 'libraryfolders.vdf'); + let libraryFolders: any = path.join(steamPath, 'steamapps', 'libraryfolders.vdf'); - if(!(await pathExist(libraryFolders))){ return null; } - libraryFolders = parse(await readFile(libraryFolders, {encoding: 'utf-8'})); + if(!(await pathExist(libraryFolders))){ return null; } + libraryFolders = parse(await readFile(libraryFolders, {encoding: 'utf-8'})); - if(!libraryFolders.libraryfolders){ return null; } - libraryFolders = libraryFolders.libraryfolders + if(!libraryFolders.libraryfolders){ return null; } + libraryFolders = libraryFolders.libraryfolders - for(const libKey in Object.keys(libraryFolders)){ - if(!libraryFolders[libKey] || !libraryFolders[libKey]["apps"]){ continue; } - if(libraryFolders[libKey]["apps"][gameId] != null){ return path.join(libraryFolders[libKey]["path"], "steamapps", "common", gameFolder); }; + for(const libKey in Object.keys(libraryFolders)){ + if(!libraryFolders[libKey] || !libraryFolders[libKey]["apps"]){ continue; } + if(libraryFolders[libKey]["apps"][gameId] != null){ return path.join(libraryFolders[libKey]["path"], "steamapps", "common", gameFolder); }; + } + return null; + } + catch(e){ + log.error(e); + return null; } - return null; } - catch(e){ - log.error(e); - return null; - } - } public openSteam(): Promise{ const process = spawn("start", ["steam://open/games"], {shell: true}); 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 18cbb5ab..60716c8e 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 @@ -25,7 +25,7 @@ export function LaunchSlide({version}: Props) { const [advancedLaunch, setAdvancedLaunch] = useState(false); const [additionalArgsString, setAdditionalArgsString] = useState(configService.get("additionnal-args") || ""); - const launchState = useObservable(bsLauncherService.launchState$); + const versionRunning = useObservable(bsLauncherService.versionRunning$); useEffect(() => { configService.set("additionnal-args", additionalArgsString); @@ -69,7 +69,7 @@ export function LaunchSlide({version}: Props) {
- +
) diff --git a/src/renderer/services/bs-launcher.service.ts b/src/renderer/services/bs-launcher.service.ts index 78523108..9e399a49 100644 --- a/src/renderer/services/bs-launcher.service.ts +++ b/src/renderer/services/bs-launcher.service.ts @@ -1,10 +1,11 @@ -import { LauchOption, LaunchResult } from "shared/models/bs-launch"; +import { LaunchOption, LaunchResult } from "shared/models/bs-launch"; import { BSVersion } from 'shared/bs-version.interface'; import { IpcService } from "./ipc.service"; import { NotificationService } from "./notification.service"; import { BsDownloaderService } from "./bs-downloader.service"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, Observable } from "rxjs"; import { NotificationResult } from "shared/models/notification/notification.model"; +import { BSLaunchErrorEvent, BSLaunchErrorType, BSLaunchEvent } from "../../shared/models/bs-launch"; export class BSLauncherService{ @@ -14,7 +15,7 @@ export class BSLauncherService{ private readonly notificationService: NotificationService; private readonly bsDownloaderService: BsDownloaderService; - public readonly launchState$: BehaviorSubject = new BehaviorSubject(null); + public readonly versionRunning$: BehaviorSubject = new BehaviorSubject(null); public static getInstance(){ if(!BSLauncherService.instance){ BSLauncherService.instance = new BSLauncherService(); } @@ -25,13 +26,13 @@ export class BSLauncherService{ this.ipcService = IpcService.getInstance(); this.notificationService = NotificationService.getInstance(); this.bsDownloaderService = BsDownloaderService.getInstance(); - this.listenBsExit(); } + // TODO REMOVE private listenBsExit(): void{ this.ipcService.watch("bs-launch.exit").subscribe(res => { - const version = this.launchState$.value; - this.launchState$.next(null); + const version = this.versionRunning$.value; + this.versionRunning$.next(null); if(res.success){ return; } this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.EXIT", desc: "notifications.bs-launch.errors.msg.EXIT", actions: [{id: "0", title: "misc.verify"}]}).then(res => { if(res === "0"){ this.bsDownloaderService.download(version, true); } @@ -41,15 +42,15 @@ export class BSLauncherService{ // TODO : Rework with shortcuts implementation - public launch(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean, additionalArgs?: string[]): Promise{ - const lauchOption: LauchOption = {debug, oculus, desktop, version, additionalArgs}; - if(this.launchState$.value){ return this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.BS_ALREADY_RUNNING"}); } - this.launchState$.next(version); + public launch_old(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean, additionalArgs?: string[]): Promise{ + const lauchOption: LaunchOption = {debug, oculus, desktop, version, additionalArgs}; + if(this.versionRunning$.value){ return this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.BS_ALREADY_RUNNING"}); } + this.versionRunning$.next(version); return this.ipcService.send("bs-launch.launch", {args: lauchOption}).then(res => { if(res.data === "LAUNCHED"){ return this.notificationService.notifySuccess({title: "notifications.bs-launch.success.titles.launching"}); } - this.launchState$.next(null); + this.versionRunning$.next(null); if(!res.success){ return this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.UNABLE_TO_LAUNCH", desc: res.error.title}); } @@ -66,6 +67,30 @@ export class BSLauncherService{ }); } + public launch(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean, additionalArgs?: string[]): Observable { + const launchState$ = this.ipcService.sendV2("bs-launch.launch", {args: {debug, oculus, desktop, version, additionalArgs}}); + + this.versionRunning$.next(version); + + launchState$.subscribe({ + next: event => { + this.notificationService.notifySuccess({title: `notifications.bs-launch.success.titles.${event.type}`, desc: `notifications.bs-launch.success.msg.${event.type}`}); + }, + error: (err: BSLaunchErrorEvent) => { + if(err.type === BSLaunchErrorType.UNKNOWN_ERROR || !Object.values(BSLaunchErrorType).includes(err.type)){ + this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.UNABLE_TO_LAUNCH"}); + } else { + this.notificationService.notifyError({title: `notifications.bs-launch.errors.titles.${err.type}`, desc: `notifications.bs-launch.errors.msg.${err.type}`}) + } + }, + complete: () => { + this.versionRunning$.next(null); + } + }) + + return launchState$; + } + } export enum LaunchMods{ diff --git a/src/shared/models/bs-launch/index.ts b/src/shared/models/bs-launch/index.ts index aec201cf..0e46b4a5 100644 --- a/src/shared/models/bs-launch/index.ts +++ b/src/shared/models/bs-launch/index.ts @@ -1,2 +1,3 @@ -export { LauchOption } from "./launch-option.interface" -export { LaunchResult } from "./launch-result.interface" \ No newline at end of file +export { LaunchOption } from "./launch-option.interface" +export { LaunchResult } from "./launch-result.interface" +export { BSLaunchErrorEvent, BSLaunchEvent, BSLaunchErrorType, BSLaunchEventType } from "./launch-event.model" \ No newline at end of file diff --git a/src/shared/models/bs-launch/launch-event.model.ts b/src/shared/models/bs-launch/launch-event.model.ts new file mode 100644 index 00000000..9f2d7858 --- /dev/null +++ b/src/shared/models/bs-launch/launch-event.model.ts @@ -0,0 +1,22 @@ +export interface BSLaunchEvent{ + type: BSLaunchEventType; + data?: unknown; +} + +export interface BSLaunchErrorEvent{ + type: BSLaunchErrorType; + data?: unknown; +} + +export enum BSLaunchErrorType{ + BS_NOT_FOUND = "EXE_NOT_FINDED", + BS_ALREADY_RUNNING = "BS_ALREADY_RUNNING", + OCULUS_NOT_RUNNING = "OCULUS_NOT_RUNNING", + BS_EXIT_ERROR = "EXIT", + UNKNOWN_ERROR = "UNKNOWN_ERROR", +} + +export enum BSLaunchEventType{ + STEAM_LAUNCHING = "STEAM_LAUNCHING", + BS_LAUNCHING = "BS_LAUNCHING", +} \ No newline at end of file diff --git a/src/shared/models/bs-launch/launch-option.interface.ts b/src/shared/models/bs-launch/launch-option.interface.ts index cad8fecf..e816b9b4 100644 --- a/src/shared/models/bs-launch/launch-option.interface.ts +++ b/src/shared/models/bs-launch/launch-option.interface.ts @@ -1,6 +1,6 @@ import { BSVersion } from "shared/bs-version.interface"; -export interface LauchOption { +export interface LaunchOption { version: BSVersion, oculus: boolean, desktop: boolean,