From cab1fb93decc5d7553bc9775900ec391c644a7d0 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sun, 5 Jan 2025 00:06:04 +0100 Subject: [PATCH 1/3] [feature] add checkbox to create a steam shortcut + fix create shortcut modal --- ...create-launch-shortcut-modal.component.tsx | 40 +++++++++++++------ .../pages/version-viewer.component.tsx | 2 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx index e38b35cf..108d07b0 100644 --- a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx @@ -1,7 +1,7 @@ import { ModalComponent, ModalExitCode } from "renderer/services/modale.service" import { BSVersion } from "shared/bs-version.interface" import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; -import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { useTranslationV2 } from "renderer/hooks/use-translation.hook"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { LaunchOption } from "shared/models/bs-launch"; import { useService } from "renderer/hooks/use-service.hook"; @@ -11,27 +11,37 @@ import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.componen import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component"; import Tippy from "@tippyjs/react"; +import { LaunchMod } from "shared/models/bs-launch/launch-option.interface"; -export const CreateLaunchShortcutModal: ModalComponent = ({resolver, options: {data}}) => { +export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, launchOption: LaunchOption }, BSVersion> = ({resolver, options: {data}}) => { const bsLauncher = useService(BSLauncherService); - const t = useTranslation(); + const { text: t } = useTranslationV2(); const color = useThemeColor("second-color"); - const [launchOptions, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data)); - const [advanced, setAdvanced] = useState(!!launchOptions.additionalArgs?.length); - const [additionalArgsString, setAdditionalArgsString] = useState(launchOptions.additionalArgs?.join("; ") ?? ""); + const [launchOption, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data)); + const [advanced, setAdvanced] = useState(!!launchOption.additionalArgs?.length); + const [additionalArgsString, setAdditionalArgsString] = useState(launchOption.additionalArgs?.join("; ") ?? ""); + const [steamShortcut, setSteamShortcut] = useState(false); const completeModal = () => { if(advanced) { - launchOptions.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length); + launchOption.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length); } else { - launchOptions.additionalArgs = undefined; + launchOption.additionalArgs = undefined; } - resolver({exitCode: ModalExitCode.COMPLETED, data: launchOptions}); + resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }}); + } + + const toogleLaunchMod = (mod: LaunchMod, enabled: boolean) => { + if(enabled) { + setLaunchOptions(prev => ({...prev, launchMods: [...prev.launchMods, mod]})); + } else { + setLaunchOptions(prev => ({...prev, launchMods: prev.launchMods.filter(m => m !== mod)})); + } } return ( @@ -49,20 +59,20 @@ export const CreateLaunchShortcutModal: ModalComponent
{data.oculus !== true && (
- setLaunchOptions({...launchOptions, oculus: e})} /> + toogleLaunchMod("oculus", e)} /> {t("pages.version-viewer.launch-mods.oculus")}
)}
- setLaunchOptions({...launchOptions, desktop: e})} /> + toogleLaunchMod("fpfc", e)} /> {t("pages.version-viewer.launch-mods.desktop")}
- setLaunchOptions({...launchOptions, debug: e})} /> + toogleLaunchMod("debug", e)} /> {t("pages.version-viewer.launch-mods.debug")} @@ -87,6 +97,12 @@ export const CreateLaunchShortcutModal: ModalComponent
+ +
+ setSteamShortcut(() => e)} /> + Créer un raccourci Steam +
+
resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" /> diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index 70b50922..9e41ca49 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -120,7 +120,7 @@ export function VersionViewer() { const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, {data: state}); if(exitCode !== ModalExitCode.COMPLETED){ return; } - lastValueFrom(bsLauncher.createLaunchShortcut(data)).then(() => { + lastValueFrom(bsLauncher.createLaunchShortcut(data.launchOption)).then(() => { notification.notifySuccess({ title: "notifications.create-launch-shortcut.success.title", desc: "notifications.create-launch-shortcut.success.msg" From 774c3a29540d68fd9b5fcbfd2ed84d5d894e9a15 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sun, 5 Jan 2025 21:33:23 +0100 Subject: [PATCH 2/3] [feature] advancement on steam shortcut --- src/main/ipcs/bs-launcher-ipcs.ts | 2 +- .../bs-launcher/bs-launcher.service.ts | 37 ++- src/main/services/steam.service.ts | 256 ++++++++++++++++-- ...create-launch-shortcut-modal.component.tsx | 21 +- .../download-maps-modal.component.tsx | 2 +- .../pages/version-viewer.component.tsx | 2 +- src/renderer/services/bs-launcher.service.ts | 4 +- src/shared/models/ipc/ipc-routes.ts | 2 +- src/shared/models/steam/shortcut.model.ts | 28 ++ 9 files changed, 310 insertions(+), 44 deletions(-) create mode 100644 src/shared/models/steam/shortcut.model.ts diff --git a/src/main/ipcs/bs-launcher-ipcs.ts b/src/main/ipcs/bs-launcher-ipcs.ts index dd2ffcd6..c1581779 100644 --- a/src/main/ipcs/bs-launcher-ipcs.ts +++ b/src/main/ipcs/bs-launcher-ipcs.ts @@ -27,7 +27,7 @@ ipc.on("bs-launch.need-start-as-admin", (_, reply) => { ipc.on("create-launch-shortcut", (args, reply) => { const bsLauncher = BSLauncherService.getInstance(); - reply(from(bsLauncher.createLaunchShortcut(args))); + reply(from(bsLauncher.createLaunchShortcut(args.options, args.steamShortcut))); }); ipc.on("bs-launch.restore-steamvr", (_, reply) => { diff --git a/src/main/services/bs-launcher/bs-launcher.service.ts b/src/main/services/bs-launcher/bs-launcher.service.ts index 94692ccd..11916a7c 100644 --- a/src/main/services/bs-launcher/bs-launcher.service.ts +++ b/src/main/services/bs-launcher/bs-launcher.service.ts @@ -22,6 +22,9 @@ import { BSVersion } from "shared/bs-version.interface"; import { BsStore } from "../../../shared/models/bs-store.enum"; import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.interface"; import { StaticConfigurationService } from "../static-configuration.service"; +import { SteamService } from "../steam.service"; +import { tryit } from "shared/helpers/error.helpers"; +import { SteamShortcut } from "shared/models/steam/shortcut.model"; export class BSLauncherService { private static instance: BSLauncherService; @@ -32,6 +35,7 @@ export class BSLauncherService { private readonly ipc: IpcService; private readonly remoteVersion: BSVersionLibService; private readonly steamLauncher: SteamLauncherService; + private readonly steam: SteamService; private readonly oculusLauncher: OculusLauncherService; private readonly staticConfig: StaticConfigurationService; @@ -51,6 +55,7 @@ export class BSLauncherService { this.steamLauncher = SteamLauncherService.getInstance(); this.oculusLauncher = OculusLauncherService.getInstance(); this.staticConfig = StaticConfigurationService.getInstance(); + this.steam = SteamService.getInstance(); this.bsmProtocolService.on("launch", link => { log.info("Launch from bsm protocol", link.toString()); @@ -195,12 +200,42 @@ export class BSLauncherService { return this.bsmProtocolService.buildLink("launch", shortcutParams).toString(); } - public async createLaunchShortcut(launchOptions: LaunchOption): Promise{ + private async getSteamShortcutData(launchOptions: LaunchOption): Promise{ + const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" "); + const shortcutIconColor = new Color(launchOptions.version.color, "hex"); + + const exePath = app.getPath("exe"); + + return { + appid: "\u0000\u0000\u0000", + AppName: shortcutName, + Exe: exePath, + StartDir: path.dirname(exePath), + LaunchOptions: this.createLaunchLink(launchOptions), + icon: await this.createShortcutPng(shortcutIconColor), + tags: [ + "BSManager", + "Beat Saber", + "VR", + ] + } as SteamShortcut; + } + + public async createLaunchShortcut(launchOptions: LaunchOption, steamShortcut?: boolean): Promise{ const shortcutUrl = this.createLaunchLink(launchOptions); const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" "); const shortcutIconColor = new Color(launchOptions.version.color, "hex"); + if(steamShortcut){ + const userId = await tryit(() => this.steam.getActiveUser()); + return this.steam.createShortcut(await this.getSteamShortcutData(launchOptions), userId.result).then(() => true).catch(e => { + log.error(e); + return false; + }); + } + + return execOnOs({ win32: async () => ( shell.writeShortcutLink(path.join(app.getPath("desktop"), `${shortcutName}.lnk`), { diff --git a/src/main/services/steam.service.ts b/src/main/services/steam.service.ts index 9d099b0d..257c270c 100644 --- a/src/main/services/steam.service.ts +++ b/src/main/services/steam.service.ts @@ -2,49 +2,55 @@ import { RegDwordValue } from "regedit-rs" import path from "path"; import { parse } from "@node-steam/vdf"; import { readFile } from "fs/promises"; -import { pathExist } from "../helpers/fs.helpers"; import log from "electron-log"; import { app, shell } from "electron"; import { getProcessId, isProcessRunning } from "main/helpers/os.helpers"; import { isElevated } from "query-process"; import { execOnOs } from "../helpers/env.helpers"; +import { pathExists, pathExistsSync, readdir, writeFile } from "fs-extra"; +import { SteamShortcut, SteamShortcutKey } from "../../shared/models/steam/shortcut.model"; const { list } = (execOnOs({ win32: () => require("regedit-rs") }, true) ?? {}) as typeof import("regedit-rs"); export class SteamService { - - private static readonly PROCESS_NAME: string = process.platform === "linux" - ? "steam-runtime-launcher-service" - : "steam.exe"; + private static readonly PROCESS_NAME: string = process.platform === "linux" ? "steam-runtime-launcher-service" : "steam.exe"; private static instance: SteamService; - private steamPath: string = ''; + private steamPath: string = ""; - private constructor(){} + private constructor() {} - public static getInstance(){ - if(!SteamService.instance){ SteamService.instance = new SteamService(); } + public static getInstance() { + if (!SteamService.instance) { + SteamService.instance = new SteamService(); + } return SteamService.instance; } - public async getActiveUser(): Promise{ + public async getActiveUser(): Promise { const res = await list("HKCU\\Software\\Valve\\Steam\\ActiveProcess"); const key = res["HKCU\\Software\\Valve\\Steam\\ActiveProcess"]; - if(!key.exists){ throw new Error("Key \"HKCU\\Software\\Valve\\Steam\\ActiveProcess\" not exist"); } + if (!key.exists) { + throw new Error('Key "HKCU\\Software\\Valve\\Steam\\ActiveProcess" not exist'); + } const registryValue = key.values.ActiveUser as RegDwordValue; - if(!registryValue){ throw new Error("Value \"ActiveUser\" not exist"); } + if (!registryValue) { + throw new Error('Value "ActiveUser" not exist'); + } return registryValue.value; } - public async isSteamRunning(): Promise{ + public async isSteamRunning(): Promise { const steamProcessRunning = await isProcessRunning(SteamService.PROCESS_NAME); - if(process.platform === "linux") { return steamProcessRunning; } + if (process.platform === "linux") { + return steamProcessRunning; + } const activeUser = await this.getActiveUser().catch(err => log.error(err)); return steamProcessRunning && !!activeUser; } - public async getSteamPid(): Promise{ + public async getSteamPid(): Promise { return getProcessId(SteamService.PROCESS_NAME); } @@ -53,26 +59,29 @@ export class SteamService { * @throws Can throw an error if the Steam process is running as admin * @returns true if the Steam process is running as administrator */ - public async isElevated(): Promise{ + public async isElevated(): Promise { const steamPid = await this.getSteamPid(); - if(!steamPid){ return false; } + if (!steamPid) { + return false; + } return isElevated(steamPid); } - public async getSteamPath(): Promise{ - - if(this.steamPath){ return this.steamPath; } + public async getSteamPath(): Promise { + if (this.steamPath) { + return this.steamPath; + } switch (process.platform) { case "linux": - this.steamPath = path.join(app.getPath('home'), '.steam', "steam"); + this.steamPath = path.join(app.getPath("home"), ".steam", "steam"); return this.steamPath; case "win32": { - const res = await list(['HKLM\\SOFTWARE\\Valve\\Steam', 'HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam']); - const win64 = res['HKLM\\SOFTWARE\\Valve\\Steam']; - const win32 = res['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam']; + const res = await list(["HKLM\\SOFTWARE\\Valve\\Steam", "HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]); + const win64 = res["HKLM\\SOFTWARE\\Valve\\Steam"]; + const win32 = res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]; if (win64.exists && win64?.values?.InstallPath?.value) { this.steamPath = win64.values.InstallPath.value as string; @@ -93,15 +102,21 @@ export class SteamService { let libraryFolders: any = path.join(steamPath, "steamapps", "libraryfolders.vdf"); - if (!(await pathExist(libraryFolders))) { return null; } + if (!(await pathExists(libraryFolders))) { + return null; + } libraryFolders = parse(await readFile(libraryFolders, { encoding: "utf-8" })); - if (!libraryFolders.libraryfolders) { return null; } + if (!libraryFolders.libraryfolders) { + return null; + } libraryFolders = libraryFolders.libraryfolders; for (const libKey in Object.keys(libraryFolders)) { - if (!libraryFolders?.[libKey]?.apps) { continue; } + if (!libraryFolders?.[libKey]?.apps) { + continue; + } if (libraryFolders[libKey].apps[gameId] != null) { return path.join(libraryFolders[libKey].path, "steamapps", "common", gameFolder); @@ -109,7 +124,6 @@ export class SteamService { } return null; - } catch (e) { log.error(e); return null; @@ -117,7 +131,6 @@ export class SteamService { } public async openSteam(): Promise { - await shell.openExternal("steam://open/games"); return new Promise((resolve, reject) => { @@ -125,7 +138,9 @@ export class SteamService { const interval = setInterval(() => { const steamRunning = this.isSteamRunning().catch(() => false); steamRunning.then(running => { - if(!running){ return; } + if (!running) { + return; + } clearInterval(interval); resolve(); }); @@ -138,4 +153,185 @@ export class SteamService { }, 60_000); }); } + + private async getUserDataFolders(): Promise { + const steamPath = await this.getSteamPath(); + + if (!steamPath) { + return []; + } + + const configPath = path.join(steamPath, "userdata"); + + if (!pathExistsSync(configPath)) { + return []; + } + + const folders = readdir(configPath, { withFileTypes: true }) + .then(entries => { + return entries.reduce((acc, entry) => { + if (entry.isDirectory() && Number.isInteger(parseInt(entry.name))) { + acc.push(path.join(configPath, entry.name)); + } + return acc; + }, []); + }) + .catch(err => { + log.error("Error while reading steam user data folders", err); + return []; + }); + + return folders; + } + + private async getShortcutsPath(userId: number): Promise { + return path.join(await this.getSteamPath(), "userdata", userId.toString(), "config", "shortcuts.vdf"); + } + + private async readShortcutsFile(shortcutsPath: string): Promise { + // Code taken from: https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts#Reading_the_shortcuts.vdf + + const rawData: string = await readFile(shortcutsPath, "utf-8"); + const startIndex = rawData.indexOf("\u0000shortcuts\u0000"); + if (startIndex < 0) { + console.error("Could not find shortcuts in shortcuts.vdf"); + return []; + } + const start = startIndex + "\u0000shortcuts\u0000".length; + const end = rawData.lastIndexOf("\u0008\u0008"); + if (end < 0 || end <= start) { + console.error("Could not find end of shortcuts in shortcuts.vdf"); + return []; + } + + const shortcutsString = rawData.substring(start, end - start); + + const result: SteamShortcut[] = []; + let currentShortcut: SteamShortcut | null = null; + let word = ""; + let key = ""; + let readingTags = false; + let tagId = -1; + + const shortcutKeysRegex = new RegExp(`(\u0001|\u0002)(${Object.values(SteamShortcutKey).join("|")})`, "i"); + + for (const c of shortcutsString) { + + if (c === "\u0000") { + if (word.endsWith(`\u0001${SteamShortcutKey.AppName}`)) { + if (currentShortcut) { + result.push(currentShortcut); + } + currentShortcut = { + AppName: "", + Exe: "", + StartDir: "", + icon: "", + LaunchOptions: "", + IsHidden: null, + tags: [], + }; + key = `\u0001${SteamShortcutKey.AppName}`; + } else if (shortcutKeysRegex.test(word)) { + key = word; + } else if (word === SteamShortcutKey.tags) { + readingTags = true; + } else if (key !== "") { + const currentKey = shortcutKeysRegex.exec(key).pop().replaceAll("\u0001", "").replaceAll("\u0002", "") as SteamShortcutKey; + if (currentShortcut && currentKey && currentKey !== SteamShortcutKey.tags) { + currentShortcut[currentKey] = (word as string & string[]); + } + key = ""; + } else if (readingTags) { + if (word.startsWith("\u0001")) { + tagId = parseInt(word.substring(1), 10); + } else if (tagId >= 0 && currentShortcut) { + currentShortcut.tags.push(word); + tagId = -1; + } else { + readingTags = false; + } + } + word = ""; + } else { + word += c; + } + } + + if (currentShortcut) { + result.push(currentShortcut); + } + + return result; + } + + private async getShortcuts(userId: number): Promise { + const shortcutsPath = await this.getShortcutsPath(userId); + return this.readShortcutsFile(shortcutsPath); + } + + private buildShortcutTagsString(tags: string[]): string { + let tagString = "\u0000tags\u0000"; + for (let i = 0; i < tags.length; i++) { + tagString += `\u0001${i}\u0000${tags[i]}\u0000`; + } + tagString += "\u0008"; + return tagString; + } + + private buildShortcutString(shortcut: SteamShortcut): string { + const getSeparator = (key: SteamShortcutKey): string => { + return key === SteamShortcutKey.IsHidden || key === SteamShortcutKey.appid ? "\u0002" : "\u0001"; + }; + + const getQuote = (key: SteamShortcutKey): string => { + return key === SteamShortcutKey.Exe || key === SteamShortcutKey.StartDir || key === SteamShortcutKey.icon ? "\"" : ""; + } + + let shortcutString = ""; + for (const key of Object.keys(shortcut)) { + console.log("KEY", key); + if(key === SteamShortcutKey.tags) { + continue; + } + + const value = shortcut[key as SteamShortcutKey]; + shortcutString += `${getSeparator(key as SteamShortcutKey)}${key}\u0000\"${value}\"\u0000`; + } + + shortcutString += `\u0000\u0000${this.buildShortcutTagsString(shortcut.tags)}`; + return shortcutString; + } + + private async writeShortcuts(shortcuts: SteamShortcut[], userId: number): Promise { + let shortcutsString = "\u0000shortcuts\u0000"; + for (let i = 0; i < shortcuts.length; i++) { + shortcutsString += `\u0000${i}\u0000`; + shortcutsString += this.buildShortcutString(shortcuts[i]); + shortcutsString += "\u0008"; + } + shortcutsString += "\u0008\u0008"; + + const shortcutsPath = await this.getShortcutsPath(userId); + await writeFile(shortcutsPath, shortcutsString, { encoding: "utf-8" }); + + } + + public async createShortcut(shortcutData: SteamShortcut, userId?: number): Promise { + const userIds = userId ? [userId] : await (async (): Promise => { + const folders = await this.getUserDataFolders(); + return folders.map(folder => parseInt(path.basename(folder))); + })(); + + for (const userId of userIds) { + const shortcuts = await this.getShortcuts(userId).catch(e => { + log.warn("Error while reading shortcuts", e); + return []; + }); + shortcuts.push(shortcutData); + await this.writeShortcuts(shortcuts, userId); + } + + log.info("Shortcut created", shortcutData); + } } diff --git a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx index 108d07b0..c8831752 100644 --- a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx @@ -6,12 +6,13 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { LaunchOption } from "shared/models/bs-launch"; import { useService } from "renderer/hooks/use-service.hook"; import { BSLauncherService } from "renderer/services/bs-launcher.service"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component"; import Tippy from "@tippyjs/react"; import { LaunchMod } from "shared/models/bs-launch/launch-option.interface"; +import { BsStore } from "shared/models/bs-store.enum"; export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, launchOption: LaunchOption }, BSVersion> = ({resolver, options: {data}}) => { @@ -25,6 +26,10 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, const [additionalArgsString, setAdditionalArgsString] = useState(launchOption.additionalArgs?.join("; ") ?? ""); const [steamShortcut, setSteamShortcut] = useState(false); + const isSteamVersion = useMemo(() => { + return data.steam || data.metadata?.store === BsStore.STEAM; + }, [data]); + const completeModal = () => { if(advanced) { @@ -97,12 +102,14 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
- -
- setSteamShortcut(() => e)} /> - Créer un raccourci Steam -
-
+ {isSteamVersion && ( + +
+ setSteamShortcut(() => e)} /> + Créer un raccourci Steam +
+
+ )}
resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" /> diff --git a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx index 15c768e5..eeb7cf57 100644 --- a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx @@ -221,7 +221,7 @@ export const DownloadMapsModal: ModalComponent - handleSortChange(sort)} /> + handleSortChange(sort)} />
{(() => { diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index 9e41ca49..d54f0fa2 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -120,7 +120,7 @@ export function VersionViewer() { const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, {data: state}); if(exitCode !== ModalExitCode.COMPLETED){ return; } - lastValueFrom(bsLauncher.createLaunchShortcut(data.launchOption)).then(() => { + lastValueFrom(bsLauncher.createLaunchShortcut(data.launchOption, data.steamShortcut)).then(() => { notification.notifySuccess({ title: "notifications.create-launch-shortcut.success.title", desc: "notifications.create-launch-shortcut.success.msg" diff --git a/src/renderer/services/bs-launcher.service.ts b/src/renderer/services/bs-launcher.service.ts index 00766b27..86da6d59 100644 --- a/src/renderer/services/bs-launcher.service.ts +++ b/src/renderer/services/bs-launcher.service.ts @@ -125,9 +125,9 @@ export class BSLauncherService { } - public createLaunchShortcut(launchOptions: LaunchOption): Observable{ + public createLaunchShortcut(launchOptions: LaunchOption, steamShortcut: boolean): Observable{ const options: LaunchOption = {...launchOptions, version: {...launchOptions.version, color: launchOptions.version.color || this.theme.getBsmColors()[1]}}; - return this.ipcService.sendV2("create-launch-shortcut", options); + return this.ipcService.sendV2("create-launch-shortcut", { options, steamShortcut }); } public restoreSteamVR(): Promise{ diff --git a/src/shared/models/ipc/ipc-routes.ts b/src/shared/models/ipc/ipc-routes.ts index 06f83794..9337b337 100644 --- a/src/shared/models/ipc/ipc-routes.ts +++ b/src/shared/models/ipc/ipc-routes.ts @@ -53,7 +53,7 @@ export interface IpcChannelMapping { "bs-installer.set-install-path": { request: { path: string, move: boolean }, response: string}; /* ** bs-launcher-ipcs ** */ - "create-launch-shortcut": { request: LaunchOption, response: boolean }; + "create-launch-shortcut": { request: { options: LaunchOption, steamShortcut?: boolean }, response: boolean }; "bs-launch.need-start-as-admin": { request: void, response: boolean }; "bs-launch.launch": { request: LaunchOption, response: BSLaunchEventData }; "bs-launch.restore-steamvr": { request: void, response: void }; diff --git a/src/shared/models/steam/shortcut.model.ts b/src/shared/models/steam/shortcut.model.ts new file mode 100644 index 00000000..c3561d26 --- /dev/null +++ b/src/shared/models/steam/shortcut.model.ts @@ -0,0 +1,28 @@ +export enum SteamShortcutKey { + AppName = "AppName", + Exe = "Exe", + StartDir = "StartDir", + appid = "appid", + icon = "icon", + ShortcutPath = "ShortcutPath", + LaunchOptions = "LaunchOptions", + IsHidden = "IsHidden", + AllowDesktopConfig = "AllowDesktopConfig", + OpenVR = "OpenVR", + Devkit = "Devkit", + DevkitGameID = "DevkitGameID", + LastPlayTime = "LastPlayTime", + FlatpakAppID = "FlatpakAppID", + tags = "tags" +} + +type BaseShortcut = { + [K in SteamShortcutKey]: K extends "tags" ? string[] : string +}; + +export interface SteamShortcut extends Partial { + // Mandatory fields + AppName: string; + Exe: string; + StartDir: string; +} From 2a99c76d094b1f652f23369db270e7f3ae3e14f2 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:24:36 +0100 Subject: [PATCH 3/3] [feature] we can now create launch shortcut in steam library --- assets/jsons/translations/de.json | 7 +- assets/jsons/translations/en.json | 7 +- assets/jsons/translations/es.json | 7 +- assets/jsons/translations/fr.json | 7 +- assets/jsons/translations/it.json | 7 +- assets/jsons/translations/ja.json | 7 +- assets/jsons/translations/ko.json | 17 ++- assets/jsons/translations/ru.json | 7 +- assets/jsons/translations/zh-tw.json | 7 +- assets/jsons/translations/zh.json | 7 +- .../bs-launcher/bs-launcher.service.ts | 35 ++--- src/main/services/steam.service.ts | 135 ++--------------- ...create-launch-shortcut-modal.component.tsx | 4 +- .../pages/version-viewer.component.tsx | 2 +- src/shared/models/steam/shortcut.model.ts | 136 +++++++++++++++++- 15 files changed, 210 insertions(+), 182 deletions(-) diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index 5f662c4e..db477c9b 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "Verknüpfung erstellt", - "msg": "Die Verknüpfung wurde auf dem Desktop erstellt." + "msg": "Die Verknüpfung wurde auf dem Desktop erstellt.", + "msg-steam": "Die Verknüpfung wurde in der Steam-Bibliothek erstellt." }, "error": { "msg": "Beim Erstellen der Verknüpfung ist ein Fehler aufgetreten." @@ -843,7 +844,9 @@ "desc": "Das Erstellen einer Verknüpfung ermöglicht es dir, Beat Saber mit den ausgewählten Optionen zu starten, ohne durch BSManager zu gehen.", "launch-options": "Startoptionen", "advanced-launch": "Erweiterter Start", - "valid-btn": "Verknüpfung erstellen" + "valid-btn": "Verknüpfung erstellen", + "create-steam-shortcut": "Steam-Verknüpfung erstellen", + "steam-shortcut-tippy": "Wenn aktiviert, wird anstelle einer Verknüpfung auf dem Desktop eine Verknüpfung in Steam erstellt." }, "connect-to-meta": { "title": "Mit Meta verbinden", diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 9deaba07..6e7c296d 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "Shortcut created", - "msg": "The shortcut has been created on the desktop." + "msg": "The shortcut has been created on the desktop.", + "msg-steam": "The shortcut has been created in the Steam library." }, "error": { "msg": "An error occurred while creating the shortcut." @@ -838,7 +839,9 @@ "desc": "Creating a shortcut will allow you to start Beat Saber with the chosen options without going through BSManager.", "launch-options": "Launch options", "advanced-launch": "Advanced launch", - "valid-btn": "Create the shortcut" + "valid-btn": "Create the shortcut", + "create-steam-shortcut": "Create a Steam shortcut", + "steam-shortcut-tippy": "If enabled, instead of creating a shortcut on the desktop, it will be created in Steam." }, "connect-to-meta": { "title": "Connect to Meta", diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 528e4939..5b489edc 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "Acceso directo creado", - "msg": "El acceso directo se ha creado en el escritorio." + "msg": "El acceso directo se ha creado en el escritorio.", + "msg-steam": "El acceso directo se ha creado en la biblioteca de Steam." }, "error": { "msg": "Se produjo un error al crear el acceso directo." @@ -843,7 +844,9 @@ "desc": "Crear un atajo te permitirá iniciar Beat Saber con las opciones seleccionadas sin pasar por BSManager.", "launch-options": "Opciones de lanzamiento", "advanced-launch": "Lanzamiento avanzado", - "valid-btn": "Crear el atajo" + "valid-btn": "Crear el atajo", + "create-steam-shortcut": "Crear un atajo directo de Steam", + "steam-shortcut-tippy": "Si está activado, en lugar de crear un atajo directo en el escritorio, se creará en Steam." }, "connect-to-meta": { "title": "Conexión a Meta", diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index e0e3c769..06929d43 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -637,7 +637,8 @@ "create-launch-shortcut": { "success": { "title": "Raccourci créé", - "msg": "Le raccourci a été créé sur le bureau." + "msg": "Le raccourci a été créé sur le bureau.", + "msg-steam": "Le raccourci a été créé dans la bibliothèque Steam." }, "error": { "msg": "Une erreur s'est produite lors de la création du raccourci." @@ -844,7 +845,9 @@ "desc": "Créer un raccourci te permettra de démarrer Beat Saber avec les options choisies sans passer par BSManager.", "launch-options": "Options de lancement", "advanced-launch": "Lancement avancé", - "valid-btn": "Créer le raccourci" + "valid-btn": "Créer le raccourci", + "create-steam-shortcut": "Créer un raccourci Steam", + "steam-shortcut-tippy": "Si activé, au lieu de créer un raccourci sur le bureau, celui-ci sera créé dans Steam." }, "connect-to-meta": { "title": "Connexion à Meta", diff --git a/assets/jsons/translations/it.json b/assets/jsons/translations/it.json index 7de34c0c..2ad7ca1f 100644 --- a/assets/jsons/translations/it.json +++ b/assets/jsons/translations/it.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "Scorciatoia creata", - "msg": "La scorciatoia è già stat creata sul desktop." + "msg": "La scorciatoia è già stat creata sul desktop.", + "msg-steam": "Il collegamento è stato creato nella libreria di Steam." }, "error": { "msg": "C'è stato un errore durante la creazione della scorciatoia." @@ -838,7 +839,9 @@ "desc": "Creare una scorciatoia ti permetterà di avviare Beat Saber con le opzioni scelte senza passare per BSManager.", "launch-options": "Opzioni di lancio", "advanced-launch": "Lancio avanzato", - "valid-btn": "Crea la scorciatoia" + "valid-btn": "Crea la scorciatoia", + "create-steam-shortcut": "Crea un scorciatoia Steam", + "steam-shortcut-tippy": "Se abilitato, invece di creare un scorciatoia sul desktop, verrà creato in Steam." }, "connect-to-meta": { "title": "Connettiti a Meta", diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 61ffe3a7..c97bac6d 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "ショートカットを作成しました", - "msg": "デスクトップにショートカットを作成されました。" + "msg": "デスクトップにショートカットを作成されました。", + "msg-steam": "ショートカットがSteamライブラリに作成されました。" }, "error": { "msg": "ショートカットの作成中にエラーが発生しました。" @@ -843,7 +844,9 @@ "desc": "ショートカットを作成すると、BSManagerを起動せずに、直接Beat Saberを起動できます。", "launch-options": "起動オプション", "advanced-launch": "高度な起動", - "valid-btn": "ショートカットを作成" + "valid-btn": "ショートカットを作成", + "create-steam-shortcut": "Steamショートカットを作成", + "steam-shortcut-tippy": "有効にすると、デスクトップにショートカットを作成する代わりに、Steam内に作成されます。" }, "connect-to-meta": { "title": "Metaに接続する", diff --git a/assets/jsons/translations/ko.json b/assets/jsons/translations/ko.json index e296c2af..c48a5857 100644 --- a/assets/jsons/translations/ko.json +++ b/assets/jsons/translations/ko.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "바로 가기 생성 완료", - "msg": "바탕 화면에 바로 가기가 생성되었습니다." + "msg": "바탕 화면에 바로 가기가 생성되었습니다.", + "msg-steam": "바로 가기가 Steam 라이브러리에 생성되었습니다." }, "error": { "msg": "바로 가기 생성 중 오류가 발생했습니다." @@ -838,12 +839,14 @@ "title": "공유 폴더 추가", "description": "「{folder}」폴더를 연결할 때 문제가 발생할 수 있습니다. 정말 추가하시겠습니까?" }, - "create-launcher-shortcut": { - "title": "런처 바로가기 만들기", - "description": "바탕화면에 실행 파일의 바로가기를 만들어 BSManager를 손쉽게 실행하세요.", - "buttons": { - "submit": "바로가기 만들기" - } + "create-launch-shortcut": { + "title": "바로가기를 생성", + "desc": "바로가기를 생성하면 BSManager를 거치지 않고 선택한 옵션으로 Beat Saber를 시작할 수 있습니다.", + "launch-options": "실행 옵션", + "advanced-launch": "고급 실행", + "valid-btn": "바로가기 생성", + "create-steam-shortcut": "Steam 바로가기를 생성", + "steam-shortcut-tippy": "활성화하면 바탕 화면에 바로가기를 생성하는 대신 Steam에 생성됩니다." }, "connect-to-meta": { "title": "Meta에 연결하기", diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index 651e11a9..5fb1a70d 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "Ярлык создан", - "msg": "Ярлык находится на рабочем столе." + "msg": "Ярлык находится на рабочем столе.", + "msg-steam": "Ярлык был создан в библиотеке Steam." }, "error": { "msg": "Ошибка создания ярлыка." @@ -843,7 +844,9 @@ "desc": "Ярлык позволит вам запустить Beat Saber без помощи BSManager.", "launch-options": "Параметры запуска", "advanced-launch": "Расширенный запуск", - "valid-btn": "Создать ярлык" + "valid-btn": "Создать ярлык", + "create-steam-shortcut": "Создать ярлык для Steam", + "steam-shortcut-tippy": "Если включено, вместо создания ярлыка на рабочем столе, он будет создан в Steam." }, "connect-to-meta": { "title": "Подключение к Meta", diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 668517a5..614af414 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "捷徑已創建", - "msg": "捷徑已在桌面上創建" + "msg": "捷徑已在桌面上創建", + "msg-steam": "捷徑已在 Steam 庫中建立。" }, "error": { "msg": "創建捷徑時發生錯誤" @@ -843,7 +844,9 @@ "desc": "創建捷徑使得你可以在不通過 BSManager 的情況下以特定選項啟動 BeatSaber", "launch-options": "啟動選項", "advanced-launch": "高級啟動", - "valid-btn": "創建捷徑" + "valid-btn": "創建捷徑", + "create-steam-shortcut": "建立 Steam 快捷方式", + "steam-shortcut-tippy": "如果啟用,將不會在桌面建立捷徑,而是在 Steam 中建立。" }, "connect-to-meta": { "title": "連接到 Meta", diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index ace849b7..0d229eae 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -636,7 +636,8 @@ "create-launch-shortcut": { "success": { "title": "快捷方式已创建", - "msg": "快捷方式已在桌面上创建" + "msg": "快捷方式已在桌面上创建", + "msg-steam": "快捷方式已在 Steam 库中创建。" }, "error": { "msg": "创建快捷方式时发生错误" @@ -843,7 +844,9 @@ "desc": "创建快捷方式使得你可以在不通过 BSManager 的情况下以特定选项启动 BeatSaber", "launch-options": "启动选项", "advanced-launch": "高级启动", - "valid-btn": "创建快捷方式" + "valid-btn": "创建快捷方式", + "create-steam-shortcut": "创建 Steam 快捷方式", + "steam-shortcut-tippy": "如果启用,将不会在桌面创建快捷方式,而是在 Steam 中创建。" }, "connect-to-meta": { "title": "连接到 Meta", diff --git a/src/main/services/bs-launcher/bs-launcher.service.ts b/src/main/services/bs-launcher/bs-launcher.service.ts index 11916a7c..6ebe7ac7 100644 --- a/src/main/services/bs-launcher/bs-launcher.service.ts +++ b/src/main/services/bs-launcher/bs-launcher.service.ts @@ -24,7 +24,6 @@ import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.int import { StaticConfigurationService } from "../static-configuration.service"; import { SteamService } from "../steam.service"; import { tryit } from "shared/helpers/error.helpers"; -import { SteamShortcut } from "shared/models/steam/shortcut.model"; export class BSLauncherService { private static instance: BSLauncherService; @@ -200,27 +199,6 @@ export class BSLauncherService { return this.bsmProtocolService.buildLink("launch", shortcutParams).toString(); } - private async getSteamShortcutData(launchOptions: LaunchOption): Promise{ - const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" "); - const shortcutIconColor = new Color(launchOptions.version.color, "hex"); - - const exePath = app.getPath("exe"); - - return { - appid: "\u0000\u0000\u0000", - AppName: shortcutName, - Exe: exePath, - StartDir: path.dirname(exePath), - LaunchOptions: this.createLaunchLink(launchOptions), - icon: await this.createShortcutPng(shortcutIconColor), - tags: [ - "BSManager", - "Beat Saber", - "VR", - ] - } as SteamShortcut; - } - public async createLaunchShortcut(launchOptions: LaunchOption, steamShortcut?: boolean): Promise{ const shortcutUrl = this.createLaunchLink(launchOptions); @@ -229,13 +207,20 @@ export class BSLauncherService { if(steamShortcut){ const userId = await tryit(() => this.steam.getActiveUser()); - return this.steam.createShortcut(await this.getSteamShortcutData(launchOptions), userId.result).then(() => true).catch(e => { + const exePath = app.getPath("exe"); + return this.steam.createShortcut({ + AppName: shortcutName, + Exe: exePath, + StartDir: path.dirname(exePath), + LaunchOptions: this.createLaunchLink(launchOptions), + icon: await this.createShortcutPng(shortcutIconColor), + OpenVR: "\u0001" + }, userId.result).then(() => true).catch(e => { log.error(e); return false; }); } - return execOnOs({ win32: async () => ( shell.writeShortcutLink(path.join(app.getPath("desktop"), `${shortcutName}.lnk`), { @@ -262,7 +247,7 @@ export class BSLauncherService { const bsPath: string = await (async () => { const bsPath = await this.localVersionService.getInstalledVersionPath(launchOption.version); return bsPath ?? this.localVersionService.getVersionPath(launchOption.version); - })().catch(e => { + })().catch((e): null => { log.error(e); return null; }); diff --git a/src/main/services/steam.service.ts b/src/main/services/steam.service.ts index 257c270c..68cc012d 100644 --- a/src/main/services/steam.service.ts +++ b/src/main/services/steam.service.ts @@ -8,7 +8,7 @@ import { getProcessId, isProcessRunning } from "main/helpers/os.helpers"; import { isElevated } from "query-process"; import { execOnOs } from "../helpers/env.helpers"; import { pathExists, pathExistsSync, readdir, writeFile } from "fs-extra"; -import { SteamShortcut, SteamShortcutKey } from "../../shared/models/steam/shortcut.model"; +import { SteamShortcut, SteamShortcutData } from "../../shared/models/steam/shortcut.model"; const { list } = (execOnOs({ win32: () => require("regedit-rs") }, true) ?? {}) as typeof import("regedit-rs"); @@ -174,9 +174,9 @@ export class SteamService { acc.push(path.join(configPath, entry.name)); } return acc; - }, []); + }, [] as string[]); }) - .catch(err => { + .catch((err): string[] => { log.error("Error while reading steam user data folders", err); return []; }); @@ -188,136 +188,20 @@ export class SteamService { return path.join(await this.getSteamPath(), "userdata", userId.toString(), "config", "shortcuts.vdf"); } - private async readShortcutsFile(shortcutsPath: string): Promise { - // Code taken from: https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts#Reading_the_shortcuts.vdf - - const rawData: string = await readFile(shortcutsPath, "utf-8"); - const startIndex = rawData.indexOf("\u0000shortcuts\u0000"); - if (startIndex < 0) { - console.error("Could not find shortcuts in shortcuts.vdf"); - return []; - } - const start = startIndex + "\u0000shortcuts\u0000".length; - const end = rawData.lastIndexOf("\u0008\u0008"); - if (end < 0 || end <= start) { - console.error("Could not find end of shortcuts in shortcuts.vdf"); - return []; - } - - const shortcutsString = rawData.substring(start, end - start); - - const result: SteamShortcut[] = []; - let currentShortcut: SteamShortcut | null = null; - let word = ""; - let key = ""; - let readingTags = false; - let tagId = -1; - - const shortcutKeysRegex = new RegExp(`(\u0001|\u0002)(${Object.values(SteamShortcutKey).join("|")})`, "i"); - - for (const c of shortcutsString) { - - if (c === "\u0000") { - if (word.endsWith(`\u0001${SteamShortcutKey.AppName}`)) { - if (currentShortcut) { - result.push(currentShortcut); - } - currentShortcut = { - AppName: "", - Exe: "", - StartDir: "", - icon: "", - LaunchOptions: "", - IsHidden: null, - tags: [], - }; - key = `\u0001${SteamShortcutKey.AppName}`; - } else if (shortcutKeysRegex.test(word)) { - key = word; - } else if (word === SteamShortcutKey.tags) { - readingTags = true; - } else if (key !== "") { - const currentKey = shortcutKeysRegex.exec(key).pop().replaceAll("\u0001", "").replaceAll("\u0002", "") as SteamShortcutKey; - if (currentShortcut && currentKey && currentKey !== SteamShortcutKey.tags) { - currentShortcut[currentKey] = (word as string & string[]); - } - key = ""; - } else if (readingTags) { - if (word.startsWith("\u0001")) { - tagId = parseInt(word.substring(1), 10); - } else if (tagId >= 0 && currentShortcut) { - currentShortcut.tags.push(word); - tagId = -1; - } else { - readingTags = false; - } - } - word = ""; - } else { - word += c; - } - } - - if (currentShortcut) { - result.push(currentShortcut); - } - - return result; - } - private async getShortcuts(userId: number): Promise { const shortcutsPath = await this.getShortcutsPath(userId); - return this.readShortcutsFile(shortcutsPath); - } + const shortcutsString = await readFile(shortcutsPath, { encoding: "utf-8" }); - private buildShortcutTagsString(tags: string[]): string { - let tagString = "\u0000tags\u0000"; - for (let i = 0; i < tags.length; i++) { - tagString += `\u0001${i}\u0000${tags[i]}\u0000`; - } - tagString += "\u0008"; - return tagString; - } - - private buildShortcutString(shortcut: SteamShortcut): string { - const getSeparator = (key: SteamShortcutKey): string => { - return key === SteamShortcutKey.IsHidden || key === SteamShortcutKey.appid ? "\u0002" : "\u0001"; - }; - - const getQuote = (key: SteamShortcutKey): string => { - return key === SteamShortcutKey.Exe || key === SteamShortcutKey.StartDir || key === SteamShortcutKey.icon ? "\"" : ""; - } - - let shortcutString = ""; - for (const key of Object.keys(shortcut)) { - console.log("KEY", key); - if(key === SteamShortcutKey.tags) { - continue; - } - - const value = shortcut[key as SteamShortcutKey]; - shortcutString += `${getSeparator(key as SteamShortcutKey)}${key}\u0000\"${value}\"\u0000`; - } - - shortcutString += `\u0000\u0000${this.buildShortcutTagsString(shortcut.tags)}`; - return shortcutString; + return SteamShortcut.parseShortcutsRawData(shortcutsString); } private async writeShortcuts(shortcuts: SteamShortcut[], userId: number): Promise { - let shortcutsString = "\u0000shortcuts\u0000"; - for (let i = 0; i < shortcuts.length; i++) { - shortcutsString += `\u0000${i}\u0000`; - shortcutsString += this.buildShortcutString(shortcuts[i]); - shortcutsString += "\u0008"; - } - shortcutsString += "\u0008\u0008"; - const shortcutsPath = await this.getShortcutsPath(userId); - await writeFile(shortcutsPath, shortcutsString, { encoding: "utf-8" }); - + const stringData = SteamShortcut.getShortcutsString(shortcuts); + await writeFile(shortcutsPath, stringData, { encoding: "utf-8" }); } - public async createShortcut(shortcutData: SteamShortcut, userId?: number): Promise { + public async createShortcut(shortcutData: SteamShortcutData, userId?: number): Promise { const userIds = userId ? [userId] : await (async (): Promise => { const folders = await this.getUserDataFolders(); return folders.map(folder => parseInt(path.basename(folder))); @@ -328,7 +212,8 @@ export class SteamService { log.warn("Error while reading shortcuts", e); return []; }); - shortcuts.push(shortcutData); + shortcuts.push(new SteamShortcut(shortcutData)); + await this.writeShortcuts(shortcuts, userId); } diff --git a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx index c8831752..0ec7966e 100644 --- a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx @@ -103,10 +103,10 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, {isSteamVersion && ( - +
setSteamShortcut(() => e)} /> - Créer un raccourci Steam + {t("modals.create-launch-shortcut.create-steam-shortcut")}
)} diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index d54f0fa2..d42e184e 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -123,7 +123,7 @@ export function VersionViewer() { lastValueFrom(bsLauncher.createLaunchShortcut(data.launchOption, data.steamShortcut)).then(() => { notification.notifySuccess({ title: "notifications.create-launch-shortcut.success.title", - desc: "notifications.create-launch-shortcut.success.msg" + desc: `notifications.create-launch-shortcut.success.${data.steamShortcut ? "msg-steam" : "msg"}` }); }).catch(() => { notification.notifyError({ diff --git a/src/shared/models/steam/shortcut.model.ts b/src/shared/models/steam/shortcut.model.ts index c3561d26..e8930aea 100644 --- a/src/shared/models/steam/shortcut.model.ts +++ b/src/shared/models/steam/shortcut.model.ts @@ -1,4 +1,4 @@ -export enum SteamShortcutKey { +enum SteamShortcutKey { AppName = "AppName", Exe = "Exe", StartDir = "StartDir", @@ -16,13 +16,141 @@ export enum SteamShortcutKey { tags = "tags" } -type BaseShortcut = { +type BaseShortcutData = { [K in SteamShortcutKey]: K extends "tags" ? string[] : string }; -export interface SteamShortcut extends Partial { - // Mandatory fields +export interface SteamShortcutData extends Partial { + // Mandatory and specific values AppName: string; Exe: string; StartDir: string; + OpenVR?: "\x01" | "\x00"; +} + +export class SteamShortcut { + + public static parseShortcutsRawData(rawData: string): SteamShortcut[] { + // Code taken from: https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts#Reading_the_shortcuts.vdf + + const startIndex = rawData.indexOf("\u0000shortcuts\u0000"); + if (startIndex < 0) { + return []; + } + const start = startIndex + "\u0000shortcuts\u0000".length; + const end = rawData.lastIndexOf("\u0008\u0008"); + if (end < 0 || end <= start) { + return []; + } + + const shortcutsString = rawData.substring(start, end - start); + + const result: SteamShortcutData[] = []; + let currentShortcut: SteamShortcutData | null = null; + let word = ""; + let key = ""; + let readingTags = false; + let tagId = -1; + + const shortcutKeysRegex = new RegExp(`[\u0001\u0002](${Object.values(SteamShortcutKey).join("|")})`, "i"); + + for (const c of shortcutsString) { + + if (c === "\u0000") { + if (word.endsWith(`\u0001${SteamShortcutKey.AppName}`)) { + if (currentShortcut) { + result.push(currentShortcut); + } + currentShortcut = { + AppName: "", + Exe: "", + StartDir: "", + icon: "", + LaunchOptions: "", + IsHidden: "\x00", + }; + key = `\u0001${SteamShortcutKey.AppName}`; + } else if (shortcutKeysRegex.test(word)) { + key = word; + } else if (word === SteamShortcutKey.tags) { + readingTags = true; + } else if (key !== "") { + const currentKey = shortcutKeysRegex.exec(key).pop().replaceAll("\u0001", "").replaceAll("\u0002", "") as SteamShortcutKey; + if (currentShortcut && currentKey && currentKey !== SteamShortcutKey.tags) { + currentShortcut[currentKey] = word.replaceAll("\"", "") as string & ("\x01" | "\x00") // Make TS happy + } + key = ""; + } else if (readingTags) { + if (word.startsWith("\u0001")) { + tagId = parseInt(word.substring(1), 10); + } else if (tagId >= 0 && currentShortcut) { + currentShortcut.tags.push(word); + tagId = -1; + } else { + readingTags = false; + } + } + word = ""; + } else { + word += c; + } + } + + if (currentShortcut) { + result.push(currentShortcut); + } + + return result.map(shortcutData => new SteamShortcut(shortcutData)); + } + + public static getShortcutsString(shortcuts: SteamShortcut[]): string { + let shortcutsString = "\u0000shortcuts\u0000"; + for (let i = 0; i < shortcuts.length; i++) { + const shortcut = shortcuts[i]; + if(!shortcut.data?.AppName || !shortcut.data?.Exe || !shortcut.data?.StartDir) { + continue; + } + shortcutsString += `\u0000${i}\u0000`; + shortcutsString += shortcut.getStringBytes(); + } + shortcutsString += "\u0008\u0008"; + return shortcutsString; + } + + private data: SteamShortcutData; + + public constructor(shortcutData: SteamShortcutData) { + this.data = shortcutData; + } + + public getStringBytes(): string { + + const isHidden = this.data?.IsHidden === "\x01"; + const allowDesktopConfig = this.data?.AllowDesktopConfig === "\x01"; + const allowOverlay = this.data?.AllowDesktopConfig === "\x01"; + const openVR = this.data?.OpenVR === "\x01"; + const devkit = this.data?.Devkit === "\x01"; + + let strShortcut = ""; + + strShortcut += `\x02appid\x00${this.data?.appid || "\x00\x00\x00"}\x00`; + strShortcut += `\x01AppName\x00${this.data?.AppName ?? ""}\x00`; + strShortcut += `\x01Exe\x00\"${this.data?.Exe ?? ""}\"\x00`; + strShortcut += `\x01StartDir\x00\"${this.data?.StartDir ?? ""}\"\x00`; + strShortcut += `\x01icon\x00${this.data?.icon ?? ""}\x00`; + strShortcut += `\x01ShortcutPath\x00\x00`; + strShortcut += `\x01LaunchOptions\x00${this.data?.LaunchOptions ?? ""}\x00`; + strShortcut += `\x02IsHidden\x00${isHidden ? "\x01" : "\x00"}\x00\x00\x00`; + strShortcut += `\x02AllowDesktopConfig\x00${allowDesktopConfig ? "\x01" : "\x00"}\x00\x00\x00`; + strShortcut += `\x02AllowOverlay\x00${allowOverlay ? "\x01" : "\x00"}\x00\x00\x00`; + strShortcut += `\x02OpenVR\x00${openVR ? "\x01" : "\x00"}\x00\x00\x00`; + strShortcut += `\x02Devkit\x00${devkit ? "\x01" : "\x00"}\x00\x00\x00`; + strShortcut += `\x01DevkitGameID\x00${this.data?.DevkitGameID ?? ""}\x00`; + strShortcut += `\x02DevkitOverrideAppID\x00\x00\x00\x00\x00`; + strShortcut += `\x02LastPlayTime\x00\x00\x00\x00\x00`; + strShortcut += `\x00tags\x00${""}\x08\x08`; + + return strShortcut; + } + }