mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[bugfix] fix oculus launch by using sideloading
This commit is contained in:
@@ -15,3 +15,4 @@ import "./bs-model-ipcs";
|
||||
import "./bs-version-download/bs-download-ipcs";
|
||||
import "./static-configuration.ipcs";
|
||||
import "./linux.ipcs.ts";
|
||||
import "./oculus.ipcs";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { OculusService } from "../services/oculus.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("is-oculus-sideloaded-apps-enabled", (_, reply) => {
|
||||
const oculusService = OculusService.getInstance();
|
||||
reply(from(oculusService.isSideLoadedAppsEnabled()));
|
||||
});
|
||||
|
||||
ipc.on("enable-oculus-sideloaded-apps", (_, reply) => {
|
||||
const oculusService = OculusService.getInstance();
|
||||
reply(from(oculusService.enableSideloadedApps()));
|
||||
});
|
||||
@@ -1,20 +1,15 @@
|
||||
import { Observable, ReplaySubject, catchError, lastValueFrom, of, take, timeout } from "rxjs";
|
||||
import { Observable, ReplaySubject } from "rxjs";
|
||||
import { StoreLauncherInterface } from "./store-launcher.interface";
|
||||
import { BSLaunchError, 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 { BS_EXECUTABLE } from "../../constants";
|
||||
import path from "path";
|
||||
import log from "electron-log";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { lstat, pathExists, readdir, readlink, rename, symlink, unlink } from "fs-extra";
|
||||
import { pathExists } from "fs-extra";
|
||||
import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { isProcessRunning } from "../../helpers/os.helpers";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { ensurePathNotAlreadyExist } from "../../helpers/fs.helpers";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { spawn } from "child_process";
|
||||
import { tryit } from "../../../shared/helpers/error.helpers";
|
||||
|
||||
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
||||
|
||||
@@ -28,7 +23,6 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
}
|
||||
|
||||
private readonly oculus: OculusService;
|
||||
private readonly pathsService: InstallationLocationService;
|
||||
private readonly util: UtilsService;
|
||||
|
||||
private readonly oculusLib$ = new ReplaySubject<string>();
|
||||
@@ -36,120 +30,11 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
private constructor() {
|
||||
super();
|
||||
this.oculus = OculusService.getInstance();
|
||||
this.pathsService = InstallationLocationService.getInstance();
|
||||
this.util = UtilsService.getInstance();
|
||||
|
||||
this.oculus.tryGetGameFolder([OCULUS_BS_DIR, OCULUS_BS_BACKUP_DIR]).then(async dirPath => {
|
||||
if(dirPath){
|
||||
return this.oculusLib$.next( path.join(dirPath, "..") );
|
||||
}
|
||||
const defaultLib = ((await this.oculus.getOculusLibs()) || []).find(lib => lib.isDefault);
|
||||
if(defaultLib?.path){ return this.oculusLib$.next(path.join(defaultLib.path, "Software")); }
|
||||
this.oculusLib$.next(null);
|
||||
}).catch(err => {
|
||||
log.error("Error while getting Oculus libs", err);
|
||||
this.oculusLib$.next(null);
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteBsSymlinks(): Promise<void> {
|
||||
const oculusLibPath = await lastValueFrom(this.oculusLib$.pipe(take(1), timeout(sToMs(30)), catchError(() => of(null))));
|
||||
|
||||
if(!oculusLibPath || !(await pathExists(oculusLibPath))){
|
||||
throw new Error("Oculus library not found, deleteBsSymlinks");
|
||||
}
|
||||
|
||||
const libContents = await readdir(oculusLibPath);
|
||||
const symlinks = (await Promise.all(libContents.map(async dir => {
|
||||
return (await lstat(path.join(oculusLibPath, dir))).isSymbolicLink() ? dir : null;
|
||||
}))).filter(Boolean);
|
||||
|
||||
log.info("Symlinks found in Oculus library", symlinks);
|
||||
|
||||
const bsSymlinks = symlinks.filter(dirent => dirent.startsWith(OCULUS_BS_DIR));
|
||||
|
||||
const bsmSymlinks = (await Promise.all(bsSymlinks.map(async symlink => {
|
||||
const symlinkPath = path.join(oculusLibPath, symlink);
|
||||
const targetPath = await readlink(symlinkPath).catch(err => log.error(err));
|
||||
|
||||
log.info("Oculus Symlink", symlink, "target", targetPath);
|
||||
|
||||
if(!targetPath){ return null; }
|
||||
|
||||
const bsmVersionsDir = path.join(this.pathsService.INSTALLATION_FOLDER, this.pathsService.VERSIONS_FOLDER);
|
||||
|
||||
if(!targetPath.includes(bsmVersionsDir)){ return null; }
|
||||
|
||||
return symlink;
|
||||
}))).filter(Boolean);
|
||||
|
||||
await Promise.all(bsmSymlinks.map(symlink => {
|
||||
log.info("Delete symlink", symlink);
|
||||
return unlink(path.join(oculusLibPath, symlink));
|
||||
}));
|
||||
}
|
||||
|
||||
private async backupOriginalBeatSaber(): Promise<void>{
|
||||
const bsFolder = await this.oculus.getGameFolder(OCULUS_BS_DIR);
|
||||
if(!bsFolder){ return; }
|
||||
const backupPath = await ensurePathNotAlreadyExist(path.join(bsFolder, "..", OCULUS_BS_BACKUP_DIR));
|
||||
log.info("Backing up original Beat Saber", bsFolder, backupPath);
|
||||
return rename(bsFolder, backupPath);
|
||||
}
|
||||
|
||||
public async restoreOriginalBeatSaber(): Promise<void>{
|
||||
const bsFolderBackupPath = await this.oculus.getGameFolder(OCULUS_BS_BACKUP_DIR);
|
||||
if(!(await pathExists(bsFolderBackupPath))){ return; }
|
||||
const originalPath = path.join(bsFolderBackupPath, "..", OCULUS_BS_DIR);
|
||||
log.info("Restoring original Beat Saber", bsFolderBackupPath, originalPath);
|
||||
return rename(bsFolderBackupPath, originalPath);
|
||||
}
|
||||
|
||||
private launchSymlinkCleaner(pid: number, symlinkPath: string){
|
||||
|
||||
log.info("Launch Symlink Cleaner", pid, symlinkPath);
|
||||
|
||||
const exeName = "oculus_symlink_cleaner.exe";
|
||||
const scriptPath = this.util.getAssetsScriptsPath();
|
||||
spawn(path.join(scriptPath, exeName), [`${pid}`, symlinkPath], { detached: true, stdio: "ignore" });
|
||||
}
|
||||
|
||||
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
|
||||
|
||||
const prepareOriginalVersion: () => Promise<string> = async () => {
|
||||
await this.restoreOriginalBeatSaber();
|
||||
const bsPath = await this.oculus.getGameFolder(OCULUS_BS_DIR);
|
||||
if(!bsPath){
|
||||
throw new Error("Oculus Beat Saber path not found");
|
||||
}
|
||||
return bsPath;
|
||||
}
|
||||
|
||||
const prepareDowngradedVersion: () => Promise<string> = async () => {
|
||||
|
||||
const originalVersionPath = await prepareOriginalVersion().catch(() => null);
|
||||
if (!originalVersionPath) {
|
||||
throw CustomError.fromError(new Error("Original Oculus Beat Saber not installed"), BSLaunchError.ORIGINAL_OCULUS_NOT_INSTALLED);
|
||||
}
|
||||
|
||||
const oculusLib = await lastValueFrom(this.oculusLib$.pipe(take(1), timeout(sToMs(30)), catchError(() => of(null))));
|
||||
|
||||
if(!oculusLib){
|
||||
throw CustomError.fromError(new Error("No Oculus library found"), BSLaunchError.OCULUS_LIB_NOT_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);
|
||||
log.info("Creating symlink", symlinkTarget, symlinkPath);
|
||||
await symlink(symlinkTarget, symlinkPath, "junction");
|
||||
|
||||
return symlinkPath;
|
||||
}
|
||||
|
||||
return new Observable<BSLaunchEventData>(obs => {
|
||||
(async () => {
|
||||
|
||||
@@ -159,11 +44,7 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
throw CustomError.fromError(new Error("Cannot start two instance of Beat Saber for Oculus"), BSLaunchError.BS_ALREADY_RUNNING);
|
||||
}
|
||||
|
||||
// Remove previously symlinks created by BSM
|
||||
await this.deleteBsSymlinks().catch(err => log.error("Error while deleting BSM symlinks", err));
|
||||
|
||||
const bsPath = await (launchOptions.version.oculus ? prepareOriginalVersion() : prepareDowngradedVersion());
|
||||
|
||||
const bsPath = await this.localVersions.getInstalledVersionPath(launchOptions.version);
|
||||
const exePath = path.join(bsPath, BS_EXECUTABLE);
|
||||
|
||||
if(!(await pathExists(exePath))){
|
||||
@@ -178,13 +59,6 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
// Launch Beat Saber
|
||||
const process = this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions));
|
||||
|
||||
if(launchOptions.version.oculus !== true && process?.process?.pid){
|
||||
const { error } = tryit(() => this.launchSymlinkCleaner(process.process.pid, bsPath));
|
||||
if(error){
|
||||
log.error("Error while launching symlink cleaner", error);
|
||||
}
|
||||
}
|
||||
|
||||
return process.exit.catch(err => {
|
||||
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
|
||||
});
|
||||
|
||||
@@ -7,14 +7,14 @@ import { shell } from "electron";
|
||||
import { isProcessRunning } from "../helpers/os.helpers";
|
||||
import { sToMs } from "../../shared/helpers/time.helpers";
|
||||
import { execOnOs } from "../helpers/env.helpers";
|
||||
import { UtilsService } from "./utils.service";
|
||||
import { exec } from "child_process";
|
||||
|
||||
const { list } = (execOnOs({ win32: () => require("regedit-rs") }, true) ?? {}) as typeof import("regedit-rs");
|
||||
|
||||
export class OculusService {
|
||||
private static instance: OculusService;
|
||||
|
||||
private oculusLibraries: OculusLibrary[];
|
||||
|
||||
public static getInstance(): OculusService {
|
||||
if (!OculusService.instance) {
|
||||
OculusService.instance = new OculusService();
|
||||
@@ -22,7 +22,14 @@ export class OculusService {
|
||||
return OculusService.instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
private readonly utils: UtilsService;
|
||||
|
||||
private oculusLibraries: OculusLibrary[];
|
||||
|
||||
private constructor() {
|
||||
this.utils = UtilsService.getInstance();
|
||||
}
|
||||
|
||||
public async getOculusLibs(): Promise<OculusLibrary[]> {
|
||||
if (process.platform !== "win32") {
|
||||
@@ -120,6 +127,67 @@ export class OculusService {
|
||||
}, sToMs(30));
|
||||
});
|
||||
}
|
||||
|
||||
public async isSideLoadedAppsEnabled(): Promise<boolean> {
|
||||
if(process.platform !== "win32"){
|
||||
log.info("Cannot check sideloaded apps on non-windows platforms");
|
||||
throw new Error("Cannot check sideloaded apps on non-windows platforms");
|
||||
}
|
||||
|
||||
const regPath = "HKLM\\SOFTWARE\\Wow6432Node\\Oculus VR, LLC\\Oculus";
|
||||
const res = await list(regPath).then(res => res[regPath]);
|
||||
|
||||
if(!res.exists){
|
||||
log.info("Registry key not found", regPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = res.values?.AllowDevSideloaded;
|
||||
|
||||
if(!value){
|
||||
log.info("Registry value not found", "AllowDevSideloaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.value === 1;
|
||||
}
|
||||
|
||||
public async enableSideloadedApps(): Promise<void> {
|
||||
if(process.platform !== "win32"){
|
||||
log.info("Cannot enable sideloaded apps on non-windows platforms");
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = await this.isSideLoadedAppsEnabled();
|
||||
|
||||
if(enabled){
|
||||
log.info("Sideloaded apps already enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
const exePath = path.join(this.utils.getAssetsScriptsPath(), "oculus-allow-dev-sideloaded.exe");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
log.info("Enabling sideloaded apps");
|
||||
const process = exec(exePath);
|
||||
process.on("exit", code => {
|
||||
if(code === 0){
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to enable sideloaded apps, exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
process.on("error", err => {
|
||||
log.error("Error while enabling sideloaded apps", err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
process.stdout.on("data", data => {
|
||||
log.info(data.toString?.() ?? data);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface OculusLibrary {
|
||||
|
||||
+11
-16
@@ -2,31 +2,26 @@ 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";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
export const OriginalOculusVersionBackupModal: ModalComponent<boolean, void> = ({ resolver }) => {
|
||||
export const EnableOculusSideloadedApps: ModalComponent<void, void> = ({ resolver }) => {
|
||||
|
||||
const t = useTranslation();
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
const submit = () => {
|
||||
resolver({ exitCode: ModalExitCode.COMPLETED, data: dontShowAgain });
|
||||
resolver({ exitCode: ModalExitCode.COMPLETED });
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="max-w-[450px] text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.original-version-backup-oculus.title")}</h1>
|
||||
<BsmImage className="mx-auto h-20" image={BeatConflict} />
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.enable-oculus-sideloaded-apps.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
|
||||
<p className="text-sm italic mb-4 font-bold">{t("modals.original-version-backup-oculus.body.must-be-installed-once")}</p>
|
||||
<p className="mb-4">{t("modals.original-version-backup-oculus.body.will-backup")}</p>
|
||||
<p className="mb-4">{t("modals.enable-oculus-sideloaded-apps.info-1")}</p>
|
||||
<p className="mb-4">{t("modals.enable-oculus-sideloaded-apps.info-2")}</p>
|
||||
<p className="mb-4">{t("modals.enable-oculus-sideloaded-apps.info-3")}</p>
|
||||
|
||||
<div className="flex flex-row justify-start items-center gap-1.5 my-4" >
|
||||
<BsmCheckbox className="relative z-[1] w-6 aspect-square" checked={dontShowAgain} onChange={setDontShowAgain} />
|
||||
<span>{t("modals.original-version-backup-oculus.not-remind-me")}</span>
|
||||
</div>
|
||||
<a className="underline mb-5 block" href="https://github.com/Zagrios/bs-manager/wiki/Activate-Oculus-sideloading" target="_blank">{t("modals.enable-oculus-sideloaded-apps.i-want-to-do-it-myself")}</a>
|
||||
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4">
|
||||
<BsmButton
|
||||
@@ -43,7 +38,7 @@ export const OriginalOculusVersionBackupModal: ModalComponent<boolean, void> = (
|
||||
className="rounded-md transition-all h-10 flex items-center justify-center"
|
||||
onClick={submit}
|
||||
withBar={false}
|
||||
text="modals.original-version-backup-oculus.understood"
|
||||
text="modals.enable-oculus-sideloaded-apps.understood"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -7,7 +7,7 @@ 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";
|
||||
import { EnableOculusSideloadedApps } from "renderer/components/modal/modal-types/enable-oculus-sideloaded-apps";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { NeedLaunchAdminModal } from "renderer/components/modal/modal-types/need-launch-admin-modal.component";
|
||||
@@ -80,6 +80,20 @@ export class BSLauncherService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async enableSideloadedAppsIfNeeded(): Promise<void> {
|
||||
if(window.electron.platform !== "win32"){ return; }
|
||||
const isSideloadedAppsEnabled = await lastValueFrom(this.ipcService.sendV2("is-oculus-sideloaded-apps-enabled"));
|
||||
if(isSideloadedAppsEnabled){ return; }
|
||||
|
||||
const modalRes = await this.modals.openModal(EnableOculusSideloadedApps);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){
|
||||
throw new Error("Enable sideloaded apps canceled");
|
||||
}
|
||||
|
||||
await lastValueFrom(this.ipcService.sendV2("enable-oculus-sideloaded-apps"));
|
||||
}
|
||||
|
||||
public doLaunch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
|
||||
return this.ipcService.sendV2("bs-launch.launch", launchOptions);
|
||||
}
|
||||
@@ -89,10 +103,9 @@ export class BSLauncherService {
|
||||
return new Observable<BSLaunchEventData>(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);
|
||||
// If downgraded from oculus and its not the official version
|
||||
if(launchOptions.version.metadata?.store === BsStore.OCULUS && !launchOptions.version.oculus){
|
||||
await this.enableSideloadedAppsIfNeeded();
|
||||
}
|
||||
|
||||
if(launchOptions.version.metadata?.store !== BsStore.OCULUS){
|
||||
|
||||
@@ -161,6 +161,10 @@ export interface IpcChannelMapping {
|
||||
/* ** linux.ipcs ** */
|
||||
"linux.verify-proton-folder": { request: void, response: boolean };
|
||||
|
||||
/* ** oculus.ipcs ** */
|
||||
"is-oculus-sideloaded-apps-enabled": { request: void, response: boolean };
|
||||
"enable-oculus-sideloaded-apps": { request: void, response: void };
|
||||
|
||||
/* ** OTHERS (if your IPC channel is not in a "-ipcs" file, put it here) ** */
|
||||
"shortcut-launch-options": { request: void, response: LaunchOption };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user