mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature] Rework of the BS launching system to prepare for shortcuts
This commit is contained in:
@@ -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<LauchOption>) => {
|
||||
const launcherService = BSLauncherService.getInstance();
|
||||
const utilsService = UtilsService.getInstance();
|
||||
// ipcMain.on('bs-launch.launch', (event, request: IpcRequest<LauchOption>) => {
|
||||
// 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<LaunchOption>, reply) => {
|
||||
const bsLauncher = BSLauncherService.getInstance();
|
||||
reply(bsLauncher.launchV2(req.args));
|
||||
});
|
||||
|
||||
@@ -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<LaunchResult>{
|
||||
public async launch(launchOptions: LaunchOption): Promise<LaunchResult>{
|
||||
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<void>{
|
||||
|
||||
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<BSLaunchEvent>{
|
||||
return new Observable<BSLaunchEvent>(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);
|
||||
})});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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<number>{
|
||||
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<string>{
|
||||
public async getSteamPath(): Promise<string>{
|
||||
|
||||
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<string>{
|
||||
try{
|
||||
const steamPath = await this.getSteamPath();
|
||||
public async getGameFolder(gameId: string, gameFolder?: string): Promise<string>{
|
||||
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<void>{
|
||||
const process = spawn("start", ["steam://open/games"], {shell: true});
|
||||
|
||||
@@ -25,7 +25,7 @@ export function LaunchSlide({version}: Props) {
|
||||
const [advancedLaunch, setAdvancedLaunch] = useState(false);
|
||||
const [additionalArgsString, setAdditionalArgsString] = useState<string>(configService.get<string>("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) {
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(launchState)} className='relative -translate-y-1/2 text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(versionRunning)} className='relative -translate-y-1/2 text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<BSVersion> = new BehaviorSubject(null);
|
||||
public readonly versionRunning$: BehaviorSubject<BSVersion> = 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<NotificationResult|string>{
|
||||
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<NotificationResult|string>{
|
||||
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<LaunchResult>("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<BSLaunchEvent> {
|
||||
const launchState$ = this.ipcService.sendV2<BSLaunchEvent, LaunchOption>("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{
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { LauchOption } from "./launch-option.interface"
|
||||
export { LaunchResult } from "./launch-result.interface"
|
||||
export { LaunchOption } from "./launch-option.interface"
|
||||
export { LaunchResult } from "./launch-result.interface"
|
||||
export { BSLaunchErrorEvent, BSLaunchEvent, BSLaunchErrorType, BSLaunchEventType } from "./launch-event.model"
|
||||
@@ -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",
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export interface LauchOption {
|
||||
export interface LaunchOption {
|
||||
version: BSVersion,
|
||||
oculus: boolean,
|
||||
desktop: boolean,
|
||||
|
||||
Reference in New Issue
Block a user