From 9b8f1a7c4233db2a19b28163ba119b730a8a996f Mon Sep 17 00:00:00 2001 From: Pierce Thompson Date: Wed, 27 Mar 2024 01:53:49 -0400 Subject: [PATCH] Launch Beat Saber with Proton on Linux --- assets/jsons/translations/en.json | 4 +- .../bs-launcher/abstract-launcher.service.ts | 7 +++ .../bs-launcher/steam-launcher.service.ts | 55 +++++++++++++++++-- .../slides/launch/launch-slide.component.tsx | 3 +- src/renderer/services/bs-launcher.service.ts | 1 + .../models/bs-launch/launch-event.model.ts | 3 +- .../bs-launch/launch-option.interface.ts | 1 + 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 43ae8f44..7f28f46d 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -381,6 +381,7 @@ "OCULUS_NOT_RUNNING": "Oculus is not running", "BS_ALREADY_RUNNING": "Beat Saber already running", "EXE_NOT_FINDED": "Missing files", + "PROTON_NOT_SET": "Proton path not set", "EXIT": "Abrupt stop", "OCULUS_LIB_NOT_FOUND": "Oculus library not found" }, @@ -391,7 +392,8 @@ "BS_ALREADY_RUNNING": "Close BeatSaber before launching it again.", "EXE_NOT_FINDED": "Some files seem to be missing, try to verify the files.", "EXIT": "BeatSaber stopped abruptly, tries to check the files.", - "OCULUS_LIB_NOT_FOUND": "Check that the Oculus application is properly installed, and that the libraries are correctly defined in Oculus." + "OCULUS_LIB_NOT_FOUND": "Check that the Oculus application is properly installed, and that the libraries are correctly defined in Oculus.", + "PROTON_NOT_SET": "Set the Proton path in settings" }, "actions": { "STEAM_NOT_RUNNING": "Launch Steam" diff --git a/src/main/services/bs-launcher/abstract-launcher.service.ts b/src/main/services/bs-launcher/abstract-launcher.service.ts index df583d1b..5382ce99 100644 --- a/src/main/services/bs-launcher/abstract-launcher.service.ts +++ b/src/main/services/bs-launcher/abstract-launcher.service.ts @@ -53,6 +53,13 @@ export abstract class AbstractLauncherService { let timoutId: NodeJS.Timeout; const exit = new Promise((resolve, reject) => { + // Don't remove, useful for debugging! + // process.stdout.on("data", (data) => { + // log.info(`BS stdout: ${data}`); + // }); + // process.stderr.on("data", (data) => { + // log.error(`BS stderr: ${data}`); + // }); process.on("error", (err) => { log.error(`Error while launching BS`, err); diff --git a/src/main/services/bs-launcher/steam-launcher.service.ts b/src/main/services/bs-launcher/steam-launcher.service.ts index a9d1c76c..65e0a93e 100644 --- a/src/main/services/bs-launcher/steam-launcher.service.ts +++ b/src/main/services/bs-launcher/steam-launcher.service.ts @@ -11,6 +11,7 @@ import { CustomError } from "../../../shared/models/exceptions/custom-error.clas import isElevated from "is-elevated"; import { UtilsService } from "../utils.service"; import { exec } from "child_process"; +import fs from 'fs'; export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{ @@ -75,7 +76,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto return new Observable(obs => {(async () => { const bsFolderPath = await this.localVersions.getInstalledVersionPath(launchOptions.version); - const exePath = path.join(bsFolderPath, BS_EXECUTABLE); + let exePath = path.join(bsFolderPath, BS_EXECUTABLE); if(!(await pathExists(exePath))){ throw CustomError.fromError(new Error(`Path not exist : ${exePath}`), BSLaunchError.BS_NOT_FOUND); @@ -102,15 +103,61 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto await this.restoreSteamVR().catch(log.error); } - const launchArgs = this.buildBsLaunchArgs(launchOptions); + let launchArgs = this.buildBsLaunchArgs(launchOptions); + const steamPath = await this.steam.getSteamPath(); + + const env = { + ...process.env, + "SteamAppId": BS_APP_ID, + "SteamOverlayGameId": BS_APP_ID, + "SteamGameId": BS_APP_ID, + }; + + // Linux setup + if (process.platform === "linux") { + // Create the compat data path if it doesn't exist. + // If the user never ran Beat Saber through steam before + // using bsmanager, it won't exist, and proton will fail + // to launch the game. + const compatDataPath = `${steamPath}/steamapps/compatdata/${BS_APP_ID}`; + if (!fs.existsSync(compatDataPath)) { + log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); + fs.mkdirSync(compatDataPath); + } + + // proton run BeatSaber.exe + launchArgs = [ + "run", + `${exePath}`, + ...launchArgs, + ]; + exePath = launchOptions.protonPath; + if (!exePath) { + throw CustomError.fromError(new Error("Proton path not set"), BSLaunchError.PROTON_NOT_SET); + } + + // Setup Proton environment variables + Object.assign(env, { + "WINEDLLOVERRIDES": "winhttp=n,b", // Required for mods to work + "STEAM_COMPAT_DATA_PATH": compatDataPath, + "STEAM_COMPAT_INSTALL_PATH": bsFolderPath, + "STEAM_COMPAT_CLIENT_INSTALL_PATH": steamPath, + "STEAM_COMPAT_APP_ID": BS_APP_ID, + // Uncomment these to create a proton log file in the Beat Saber install directory. + // "PROTON_LOG": 1, + // "PROTON_LOG_DIR": bsFolderPath, + }); + } obs.next({type: BSLaunchEvent.BS_LAUNCHING}); + const spawnOpts = { env, cwd: bsFolderPath }; + const launchPromise = !launchOptions.admin ? ( - this.launchBs(exePath, launchArgs, { env: {...process.env, "SteamAppId": BS_APP_ID} }).exit + this.launchBs(exePath, launchArgs, spawnOpts).exit ) : ( new Promise(resolve => { - const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${exePath}" ${launchArgs.join(" ")}`, { env: {...process.env, "SteamAppId": BS_APP_ID} }); + const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${exePath}" ${launchArgs.join(" ")}`, spawnOpts); adminProcess.on("error", err => { log.error("Error while starting BS as Admin", err); resolve(-1) 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 bbe412af..af517204 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 @@ -68,7 +68,8 @@ export function LaunchSlide({ version }: Props) { oculus: version.oculus ? false : oculusMode, desktop: desktopMode, debug: debugMode, - additionalArgs + additionalArgs, + protonPath: bsLauncherService.getProtonPath(), }); return lastValueFrom(launch$).catch(() => {}); diff --git a/src/renderer/services/bs-launcher.service.ts b/src/renderer/services/bs-launcher.service.ts index 1769c9de..248870b9 100644 --- a/src/renderer/services/bs-launcher.service.ts +++ b/src/renderer/services/bs-launcher.service.ts @@ -93,6 +93,7 @@ export class BSLauncherService { } public doLaunch(launchOptions: LaunchOption): Observable{ + launchOptions.protonPath = this.getProtonPath(); return this.ipcService.sendV2("bs-launch.launch", launchOptions); } diff --git a/src/shared/models/bs-launch/launch-event.model.ts b/src/shared/models/bs-launch/launch-event.model.ts index c653feb8..29663132 100644 --- a/src/shared/models/bs-launch/launch-event.model.ts +++ b/src/shared/models/bs-launch/launch-event.model.ts @@ -14,6 +14,7 @@ export enum BSLaunchError{ OCULUS_NOT_RUNNING = "OCULUS_NOT_RUNNING", BS_EXIT_ERROR = "EXIT", OCULUS_LIB_NOT_FOUND = "OCULUS_LIB_NOT_FOUND", + PROTON_NOT_SET = "PROTON_NOT_SET", UNKNOWN_ERROR = "UNKNOWN_ERROR", } @@ -27,4 +28,4 @@ export enum BSLaunchWarning{ UNABLE_TO_LAUNCH_STEAM = "UNABLE_TO_LAUNCH_STEAM", } -export type BSLaunchEventType = BSLaunchEvent | BSLaunchWarning; \ No newline at end of file +export type BSLaunchEventType = BSLaunchEvent | BSLaunchWarning; diff --git a/src/shared/models/bs-launch/launch-option.interface.ts b/src/shared/models/bs-launch/launch-option.interface.ts index 21056c19..3731143b 100644 --- a/src/shared/models/bs-launch/launch-option.interface.ts +++ b/src/shared/models/bs-launch/launch-option.interface.ts @@ -7,4 +7,5 @@ export interface LaunchOption { debug?: boolean, additionalArgs?: string[], admin?: boolean, + protonPath?: string, }