diff --git a/package.json b/package.json index 10fcbf7d8..49bfe983d 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "modern-normalize": "^3.0.1", "moment": "^2.29.1", "node-ipc": "^12.0.0", + "plist": "^3.1.0", "quasar": "^2.18.6", "quill": "^1.3.7", "sanitize-filename": "^1.6.3", @@ -79,6 +80,7 @@ "@types/jsdom": "^21.1.7", "@types/lodash.debounce": "^4.0.7", "@types/node": "^20.19.1", + "@types/plist": "^3.0.5", "@types/quill": "^2.0.3", "@types/sinon": "^10.0.2", "@types/unzipper": "^0.10.1", diff --git a/public/linux_wrapper.sh b/public/linux_wrapper.sh index d7c79ba53..58a57dde4 100644 --- a/public/linux_wrapper.sh +++ b/public/linux_wrapper.sh @@ -1,12 +1,15 @@ #!/bin/sh -# Linux wrapper script for r2modman +# Linux/MacOS wrapper script for r2modman +# NOTE: The name `linux_wrapper.sh` is kept to avoid breaking existing installs despite this working on Mac too # Written by Naomi Calabretta (blame her if something does not work) a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BASEDIR=$(cd "$a" || exit; pwd -P) R2PROFILE="" R2STARTSERVER="" +R2OS="$(uname -s)" +echo "[R2MODMAN WRAPPER] Running on $R2OS" i=0; max=$# while [ $i -lt $max ]; do @@ -17,7 +20,7 @@ while [ $i -lt $max ]; do shift i=$((i+1)) else - echo "[R2MODMAN LINUX WRAPPER] Warning: --r2profile value is empty!" + echo "[R2MODMAN WRAPPER] Warning: --r2profile value is empty!" fi ;; --server) @@ -32,16 +35,38 @@ while [ $i -lt $max ]; do done if [ -z "$R2PROFILE" ]; then - echo "[R2MODMAN LINUX WRAPPER] Launching vanilla!" + echo "[R2MODMAN WRAPPER] Launching vanilla!" exec "$@" -fi +else + # Some old versions of run_bepinex.sh require being in the same directory as BepInEx + cd "$BASEDIR/profiles/$R2PROFILE/" -[ -n "$R2STARTSERVER" ] && exec "$BASEDIR/profiles/$R2PROFILE/start_server_bepinex.sh" "$@" || true + [ -n "$R2STARTSERVER" ] && exec "$BASEDIR/profiles/$R2PROFILE/start_server_bepinex.sh" "$@" || true -if test -f "$BASEDIR/profiles/$R2PROFILE/run_bepinex.sh"; then - exec "$BASEDIR/profiles/$R2PROFILE/run_bepinex.sh" "$@" -elif test -f "$BASEDIR/profiles/$R2PROFILE/run_umm.sh"; then - exec "$BASEDIR/profiles/$R2PROFILE/run_umm.sh" "$@" -else - exec "$BASEDIR/profiles/$R2PROFILE/start_game_bepinex.sh" "$@" + if test -f "$BASEDIR/profiles/$R2PROFILE/run_bepinex.sh"; then + echo "[R2MODMAN WRAPPER] Launching BepInEx! (via run_bepinex.sh)" + exec "$BASEDIR/profiles/$R2PROFILE/run_bepinex.sh" "$@" + elif test -f "$BASEDIR/profiles/$R2PROFILE/run_umm.sh"; then + echo "[R2MODMAN WRAPPER] Launching Unity Mod Loader!" + exec "$BASEDIR/profiles/$R2PROFILE/run_umm.sh" "$@" + elif test -f "$BASEDIR/profiles/$R2PROFILE/start_game_bepinex.sh"; then + echo "[R2MODMAN WRAPPER] Launching BepInEx! (via start_game_bepinex.sh)" + case $R2OS in + Linux*) + exec "$BASEDIR/profiles/$R2PROFILE/start_game_bepinex.sh" "$@" + ;; + # start_game_bepinex.sh doesn't inject Doorstop on MacOS, so we need to do that ourselves before running it + Darwin*) + exec sh <<' END' -s -- "$BASEDIR/profiles/$R2PROFILE" "$@" + BEPINEXDIR="$1"; shift + export DYLD_LIBRARY_PATH="${BEPINEXDIR}/doorstop_libs${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + export DYLD_INSERT_LIBRARIES="libdoorstop_x64.dylib${DYLD_INSERT_LIBRARIES:+:$DYLD_INSERT_LIBRARIES}" + source "${BEPINEXDIR}/start_game_bepinex.sh" "$@" + END + ;; + esac + else + echo "[R2MODMAN WRAPPER] Error: Tried to launch modloader, but could not find any supported launchers!" + exit 1 + fi fi diff --git a/public/macos_proxy b/public/macos_proxy deleted file mode 100755 index 4624c8eba..000000000 Binary files a/public/macos_proxy and /dev/null differ diff --git a/public/macos_wrapper.sh b/public/macos_wrapper.sh deleted file mode 100644 index 6926219f5..000000000 --- a/public/macos_wrapper.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh - -# Linux wrapper script for r2modman -# Written by Naomi Calabretta (blame her if something does not work) - -a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BASEDIR=$(cd "$a" || exit; pwd -P) - -R2PROFILE="" -R2STARTSERVER="" - -i=0; max=$# -while [ $i -lt $max ]; do - case $1 in - --r2profile) - if [ -n "$2" ]; then - R2PROFILE="$2" - shift - i=$((i+1)) - else - echo "[R2MODMAN LINUX WRAPPER] Warning: --r2profile value is empty!" - fi - ;; - --server) - R2STARTSERVER="true" - ;; - *) - set -- "$@" "$1" - ;; - esac - shift - i=$((i+1)) -done - -if [ -z "$R2PROFILE" ]; then - echo "[R2MODMAN LINUX WRAPPER] Launching vanilla!" - exec "$@" -fi - -[ -n "$R2STARTSERVER" ] && exec "$BASEDIR/profiles/$R2PROFILE/start_server_bepinex.sh" "$@" || true - -if test -f "$BASEDIR/profiles/$R2PROFILE/run_bepinex.sh"; then - exec "$BASEDIR/profiles/$R2PROFILE/run_bepinex.sh" "$@" -else - exec "$BASEDIR/profiles/$R2PROFILE/start_game_bepinex.sh" "$@" -fi diff --git a/src/components/computed/WrapperArguments.ts b/src/components/computed/WrapperArguments.ts index 389481107..c3a044d8a 100644 --- a/src/components/computed/WrapperArguments.ts +++ b/src/components/computed/WrapperArguments.ts @@ -19,10 +19,10 @@ function determineWrapperArguments() { const _ = activeGame.value; let wrapperPath = ""; - if (isFlatpakExecutable.value) { + if (isFlatpakExecutable.value || appWindow.getPlatform() === 'darwin') { wrapperPath = `${path.join(PathResolver.MOD_ROOT, 'web_start_wrapper.sh')}` } else { - wrapperPath = path.join(PathResolver.MOD_ROOT, appWindow.getPlatform() === 'darwin' ? 'macos_proxy' : 'linux_wrapper.sh'); + wrapperPath = path.join(PathResolver.MOD_ROOT, 'linux_wrapper.sh'); } return `"${wrapperPath}" %command%`; } diff --git a/src/pages/LinuxNativeGameSetup.vue b/src/pages/LinuxNativeGameSetup.vue index a57248230..a6ad02f3c 100644 --- a/src/pages/LinuxNativeGameSetup.vue +++ b/src/pages/LinuxNativeGameSetup.vue @@ -8,9 +8,16 @@

The Flatpak version of {{ appName }} now uses a different wrapper script.

You must update your launch arguments to support this.

+ +
+
+

It looks like you've previously set launch arguments.

+

The MacOS version of {{ appName }} now uses a different wrapper script.

+

You must update your launch arguments to support this.

+
- To be able to launch {{ activeGame.displayName }} on Linux, you must first setup your Steam launch options correctly.
+ To be able to launch {{ activeGame.displayName }} on {{ platformName }}, you must first setup your Steam launch options correctly.
This needs to be done because of how the BepInEx injection works on Unix systems.

Please copy and paste the following to your {{ activeGame.displayName }} launch options:
@@ -33,6 +40,7 @@ import {ComputedWrapperLaunchArguments, WineDllOverridesValue} from '../componen import InteractionProviderImpl from '../r2mm/system/InteractionProviderImpl'; import appWindow from '../providers/node/app/app_window'; import { + areAnyOldMacWrapperArgumentsProvided, areAnyWrapperArgumentsProvided, isManagerRunningOnFlatpak } from '../utils/LaunchUtils'; @@ -48,7 +56,8 @@ const platformName = computed(() => appWindow.getPlatform() === 'darwin' const finalArgs = computed(() => { let result = ''; if (WineDllOverridesValue.value) { - result = `${WineDllOverridesValue.value} `; + // Using env to set environment variables is needed for Steam on MacOS compatibility. + result = `/usr/bin/env ${WineDllOverridesValue.value} `; } return result + ComputedWrapperLaunchArguments.value; }); @@ -59,6 +68,9 @@ areAnyWrapperArgumentsProvided(activeGame.value).then(value => alreadyHadValuesS const isFlatpak = ref(false); isManagerRunningOnFlatpak().then(value => isFlatpak.value = value); +const hasOldMacWrapperSet = ref(false); +areAnyOldMacWrapperArgumentsProvided(activeGame.value).then(value => hasOldMacWrapperSet.value = value) + function copy(){ let range = document.createRange(); range.selectNode(document.getElementById('copyableArgs') as Node); diff --git a/src/pages/Splash.vue b/src/pages/Splash.vue index 12ec55089..b589330e6 100644 --- a/src/pages/Splash.vue +++ b/src/pages/Splash.vue @@ -164,8 +164,12 @@ async function moveToNextScreen() { } } } else if (appWindow.getPlatform() === 'darwin') { + const activeGame: Game = store.state.activeGame; await ensureWrapperInGameFolder('linux_wrapper.sh'); - return router.push({name: 'linux'}); + await ensureWrapperInGameFolder('web_start_wrapper.sh'); + if (!(await areWrapperArgumentsProvided(activeGame))) { + return router.push({name: 'linux'}); + } } return router.push({name: 'profiles'}); } diff --git a/src/providers/generic/game/directory_resolver/win/EGSDirectoryResolver.ts b/src/providers/generic/game/directory_resolver/win/EGSDirectoryResolver.ts index 68a9d8829..7aa488fbc 100644 --- a/src/providers/generic/game/directory_resolver/win/EGSDirectoryResolver.ts +++ b/src/providers/generic/game/directory_resolver/win/EGSDirectoryResolver.ts @@ -6,6 +6,7 @@ import FsProvider from '../../../../../providers/generic/file/FsProvider'; import EgsInstallationListItem from '../../../../../providers/generic/game/directory_resolver/win/EgsInstallationListItem'; import path from '../../../../../providers/node/path/path'; +import appWindow from "../../../../node/app/app_window"; export default class EGSDirectoryResolver extends GameDirectoryResolverProvider { @@ -20,7 +21,11 @@ export default class EGSDirectoryResolver extends GameDirectoryResolverProvider Unsure if there's a way to pull this information from the registry. */ - const datFilePath = path.join("C:", "ProgramData", "Epic", "UnrealEngineLauncher", "LauncherInstalled.dat"); + const datFilePaths = { + 'win32': path.join('C:', 'ProgramData', 'Epic', 'UnrealEngineLauncher', 'LauncherInstalled.dat'), + 'darwin': path.join(await window.app.getAppDataDirectory(), 'Epic', 'UnrealEngineLauncher', 'LauncherInstalled.dat'), + } + const datFilePath = datFilePaths[appWindow.getPlatform() as keyof typeof datFilePaths]; if (await FsProvider.instance.exists(datFilePath)) { try { const datBuffer = JSON.parse((await FsProvider.instance.readFile(datFilePath)).toString()); diff --git a/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts b/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts index 3e2349253..64cdb53c2 100644 --- a/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts +++ b/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts @@ -14,6 +14,7 @@ import DirectGameRunner from '../../../../r2mm/launching/runners/multiplatform/D import SteamGameRunner_Linux from '../../../../r2mm/launching/runners/linux/SteamGameRunner_Linux'; import SteamGameRunner_Darwin from '../../../../r2mm/launching/runners/darwin/SteamGameRunner_Darwin'; import EgsGameRunner from '../../../../r2mm/launching/runners/multiplatform/EgsGameRunner'; +import EgsGameRunner_Darwin from "../../../../r2mm/launching/runners/darwin/EgsGameRunner_Darwin"; import XboxGamePassGameRunner from '../../../../r2mm/launching/runners/windows/XboxGamePassGameRunner'; import appWindow from '../../../node/app/app_window'; @@ -50,7 +51,7 @@ const DIRECT_RUNNERS = { const EGS_RUNNERS = { "win32": new EgsGameRunner(), "linux": new EgsGameRunner(), - "darwin": new EgsGameRunner(), + "darwin": new EgsGameRunner_Darwin(), } const XBOX_RUNNERS = { @@ -99,7 +100,7 @@ const RESOLVERS: ResolverType = { [Platform.EPIC_GAMES_STORE]: { "win32": new EGSDirectoryResolver(), "linux": new DRMFreeDirectoryResolver(), - "darwin": new DRMFreeDirectoryResolver(), + "darwin": new EGSDirectoryResolver(), }, [Platform.OCULUS_STORE]: { "win32": new DRMFreeDirectoryResolver(), diff --git a/src/r2mm/launching/runners/darwin/EgsGameRunner_Darwin.ts b/src/r2mm/launching/runners/darwin/EgsGameRunner_Darwin.ts new file mode 100644 index 000000000..e0059554d --- /dev/null +++ b/src/r2mm/launching/runners/darwin/EgsGameRunner_Darwin.ts @@ -0,0 +1,26 @@ +import R2Error from "../../../../model/errors/R2Error"; +import Game from "../../../../model/game/Game"; +import Profile from "../../../../model/Profile"; +import GameRunnerProvider from "../../../../providers/generic/game/GameRunnerProvider"; +import DirectGameRunner from "../multiplatform/DirectGameRunner"; + +export default class EgsGameRunner_Darwin extends GameRunnerProvider { + // Epic doesn't allow changing the executable run for an app like Steam does, + // so if we want to use the wrapper for BepInEx support we need to call it directly. + // As such, this class literally just launches Epic and then forwards the request to DirectGameRunner. + private directGameRunner = new DirectGameRunner() + + public override getGameArguments(game: Game, profile: Profile): Promise { + return this.directGameRunner.getGameArguments(game, profile); + } + + public override startModded(game: Game, profile: Profile): Promise { + window.electron.openExternal('com.epicgames.launcher://'); + return this.directGameRunner.startModded(game, profile); + } + + public override startVanilla(game: Game, profile: Profile): Promise { + window.electron.openExternal('com.epicgames.launcher://'); + return this.directGameRunner.startVanilla(game, profile); + } +} diff --git a/src/r2mm/launching/runners/darwin/SteamGameRunner_Darwin.ts b/src/r2mm/launching/runners/darwin/SteamGameRunner_Darwin.ts index c70dc2fdd..ab6c60d02 100644 --- a/src/r2mm/launching/runners/darwin/SteamGameRunner_Darwin.ts +++ b/src/r2mm/launching/runners/darwin/SteamGameRunner_Darwin.ts @@ -11,6 +11,9 @@ import LoggerProvider, { LogSeverity } from '../../../../providers/ror2/logging/ import ChildProcess from '../../../../providers/node/child_process/child_process'; import GameInstructions from '../../instructions/GameInstructions'; import GameInstructionParser from '../../instructions/GameInstructionParser'; +import PathResolver from "../../../manager/PathResolver"; +import { ensureScriptsAreAllExecutable } from "../../../../utils/LaunchUtils"; +import { parse as parseShell } from "shell-quote"; export default class SteamGameRunner_Darwin extends GameRunnerProvider { @@ -35,7 +38,7 @@ export default class SteamGameRunner_Darwin extends GameRunnerProvider { async start(game: Game, args: string[]): Promise { const settings = await ManagerSettings.getSingleton(game); const steamDir = await GameDirectoryResolverProvider.instance.getSteamDirectory(); - if(steamDir instanceof R2Error) { + if (steamDir instanceof R2Error) { return steamDir; } @@ -51,12 +54,21 @@ export default class SteamGameRunner_Darwin extends GameRunnerProvider { "This should be the default location. If there are others, please let me know."); } - try{ - const mappedArgs = args.map(value => `"${value}"`).join(' '); - const cmd = `"${steamExecutable}/Contents/MacOS/steam_osx" -applaunch ${game.activePlatform.storeIdentifier} ${mappedArgs} ${settings.getContext().gameSpecific.launchParameters}`; + // The wrapper may call other scripts, so they need to all be executable. + await ensureScriptsAreAllExecutable(); + + try { + const lineArgs = [ + `${PathResolver.MOD_ROOT}/linux_wrapper.sh`, + ...args, + ...parseShell(settings.getContext().gameSpecific.launchParameters) + ] + await FsProvider.instance.writeFile(path.join(PathResolver.MOD_ROOT, 'wrapper_args.txt'), lineArgs.join('\n')) + + const cmd = `open steam://run/${game.activePlatform.storeIdentifier}`; LoggerProvider.instance.Log(LogSeverity.INFO, `Running command: ${cmd}`); await ChildProcess.exec(cmd); - } catch(err){ + } catch (err) { LoggerProvider.instance.Log(LogSeverity.ACTION_STOPPED, 'Error was thrown whilst starting the game'); LoggerProvider.instance.Log(LogSeverity.ERROR, (err as Error).message); throw new R2Error('Error starting Steam', (err as Error).message, 'Ensure that the Steam folder has been set correctly in the settings'); diff --git a/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts b/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts index 996b4a851..7414a577d 100644 --- a/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts +++ b/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts @@ -12,7 +12,7 @@ import childProcess from '../../../../providers/node/child_process/child_process import GameInstructions from '../../instructions/GameInstructions'; import GameInstructionParser from '../../instructions/GameInstructionParser'; import {PackageLoader} from '../../../../model/schema/ThunderstoreSchema'; -import {getDeterminedLaunchType, isManagerRunningOnFlatpak} from '../../../../utils/LaunchUtils'; +import {ensureScriptsAreAllExecutable, getDeterminedLaunchType, isManagerRunningOnFlatpak} from '../../../../utils/LaunchUtils'; import {LaunchType} from '../../../../model/real_enums/launch/LaunchType'; import InteractionProvider from "../../../../providers/ror2/system/InteractionProvider"; import PathResolver from "../../../manager/PathResolver"; @@ -42,18 +42,8 @@ export default class SteamGameRunner_Linux extends GameRunnerProvider { } proxyArgs['WINEDLLOVERRIDES'] = `"${proxyDll}=n,b"` } else { - // If sh files aren't executable then the wrapper will fail. - const shFiles = (await FsProvider.instance.readdir(await FsProvider.instance.realpath(Profile.getActiveProfile().getProfilePath()))) - .filter(value => value.endsWith(".sh")); - - try { - for (const shFile of shFiles) { - await FsProvider.instance.chmod(await FsProvider.instance.realpath(Profile.getActiveProfile().joinToProfilePath(shFile)), 0o755); - } - } catch (e) { - const err: Error = e as Error; - return new R2Error("Failed to make sh file executable", err.message, "You may need to run the manager with elevated privileges."); - } + // The wrapper may call other scripts, so they need to all be executable. + await ensureScriptsAreAllExecutable(); } const args = await this.getGameArguments(game, profile); diff --git a/src/r2mm/launching/runners/multiplatform/DirectGameRunner.ts b/src/r2mm/launching/runners/multiplatform/DirectGameRunner.ts index 4bd936b8e..192de3b09 100644 --- a/src/r2mm/launching/runners/multiplatform/DirectGameRunner.ts +++ b/src/r2mm/launching/runners/multiplatform/DirectGameRunner.ts @@ -9,6 +9,12 @@ import GameDirectoryResolverProvider from '../../../../providers/ror2/game/GameD import FsProvider from '../../../../providers/generic/file/FsProvider'; import LoggerProvider, { LogSeverity } from '../../../../providers/ror2/logging/LoggerProvider'; import ChildProcess from '../../../../providers/node/child_process/child_process'; +import path from "../../../../providers/node/path/path"; +import appWindow from "../../../../providers/node/app/app_window"; +import PathResolver from "../../../manager/PathResolver"; +import plist from 'plist'; +import { ensureScriptsAreAllExecutable } from "../../../../utils/LaunchUtils"; +import { parse as parseShell } from "shell-quote"; export default class DirectGameRunner extends GameRunnerProvider { @@ -43,11 +49,36 @@ export default class DirectGameRunner extends GameRunnerProvider { const gameExecutable = (await FsProvider.instance.readdir(gameDir)) .filter((x: string) => game.exeName.includes(x))[0]; + if (gameExecutable === undefined) { + const message = game.exeName.length > 1 ? + `Could not find any of "${game.exeName.join('", "')}"` : + `Could not find "${game.exeName[0]}"`; + const r2err = new R2Error( + `${game.displayName} folder is invalid`, + message, + 'Ensure that the game folder has been set correctly in the settings' + ); + return reject(r2err); + } + + const gameExecutablePath = await getRealExecutablePath(path.join(gameDir, gameExecutable)); + const mappedArgs = args.map(value => `"${value}"`).join(' '); - LoggerProvider.instance.Log(LogSeverity.INFO, `Running command: ${gameDir}/${gameExecutable} ${mappedArgs} ${settings.getContext().gameSpecific.launchParameters}`); + // Linux and Mac need a wrapper for a few loaders like BepInEx. + let wrapper = ''; + if (['linux', 'darwin'].includes(appWindow.getPlatform())) { + await FsProvider.instance.writeFile(path.join(PathResolver.MOD_ROOT, 'wrapper_args.txt'), `${PathResolver.MOD_ROOT}/linux_wrapper.sh`); + wrapper = `"${path.join(PathResolver.MOD_ROOT, 'web_start_wrapper.sh')}" `; - const childProcess = ChildProcess.exec(`"${gameExecutable}" ${mappedArgs} ${settings.getContext().gameSpecific.launchParameters}`, { + // The wrapper may call other scripts, so they need to all be executable. + ensureScriptsAreAllExecutable(); + } + + const command = `${wrapper}"${gameExecutablePath}" ${mappedArgs} ${settings.getContext().gameSpecific.launchParameters}`; + LoggerProvider.instance.Log(LogSeverity.INFO, `Running command: ${command}`); + + const childProcess = ChildProcess.exec(command, { cwd: gameDir, windowsHide: false, }, (err => { @@ -62,3 +93,33 @@ export default class DirectGameRunner extends GameRunnerProvider { }); } } + +async function getRealExecutablePath(executablePath: string) { + // MacOS .app files are actually folders, with the real executable being defined in a config file. + if (executablePath.endsWith('.app') && (await FsProvider.instance.stat(executablePath)).isDirectory()) { + const infoPlist = await FsProvider.instance.readFile(path.join(executablePath, 'Contents', 'Info.plist')) + .then(buffer => plist.parse(buffer.toString('utf-8')) as plist.PlistObject) + .catch(err => new R2Error( + `${executablePath} is invalid`, + err.toString(), + 'Ensure that the game folder has been set correctly in the settings' + )); + if (infoPlist instanceof R2Error) { + throw infoPlist; + } + + const realExecutableName = infoPlist['CFBundleExecutable'] + if (typeof realExecutableName !== 'string') { + throw new R2Error( + `${executablePath} is invalid`, + 'Could not find an actual executable inside app', + 'Ensure that the game folder has been set correctly in the settings' + ); + } + + const realExecutablePath = path.join(executablePath, 'Contents', 'MacOS', realExecutableName); + return realExecutablePath; + } else { + return executablePath; + } +} diff --git a/src/r2mm/manager/darwin/DarwinGameDirectoryResolver.ts b/src/r2mm/manager/darwin/DarwinGameDirectoryResolver.ts index c588ad2d7..0219fc913 100644 --- a/src/r2mm/manager/darwin/DarwinGameDirectoryResolver.ts +++ b/src/r2mm/manager/darwin/DarwinGameDirectoryResolver.ts @@ -10,6 +10,7 @@ import * as vdf from '@node-steam/vdf'; import VdfParseError from '../../../model/errors/Vdf/VdfParseError'; import LoggerProvider, { LogSeverity } from '../../../providers/ror2/logging/LoggerProvider'; import GameManager from '../../../model/game/GameManager'; +import { getPropertyFromPath } from "../../../utils/Common"; export default class DarwinGameDirectoryResolver extends GameDirectoryResolverProvider { @@ -152,4 +153,40 @@ export default class DarwinGameDirectoryResolver extends GameDirectoryResolverPr } } + // TODO: Move this to Steam Utils when the multiple store refactor is made + public async getLaunchArgs(game: Game): Promise { + const steamDir = await this.getSteamDirectory(); + if (steamDir instanceof R2Error) return steamDir; + + const loginUsers = vdf.parse((await FsProvider.instance.readFile(path.join(steamDir, 'config', 'loginusers.vdf'))).toString()); + let userSteamID64 = ''; + for(let _id in loginUsers.users) { + if(loginUsers.users[_id].MostRecent == 1) { + userSteamID64 = _id; + break; + } + } + + if(userSteamID64.length === 0) return new R2Error( + 'Unable to get the current Steam User ID', + 'Please try again', + null + ); + + const userAccountID = (BigInt(userSteamID64) & BigInt(0xFFFFFFFF)).toString(); + + const localConfig = vdf.parse((await FsProvider.instance.readFile(path.join(steamDir, 'userdata', userAccountID, 'config', 'localconfig.vdf'))).toString()); + + //find apps in one of possible locations. + const apps = getPropertyFromPath(localConfig, [ + 'UserLocalConfigStore.Software.Valve.Steam.Apps', + 'UserLocalConfigStore.Software.Valve.Steam.apps', + 'UserLocalConfigStore.Software.Valve.steam.apps' + ]); + if (!apps) { + console.warn('Steam.Apps not found !'); + } + + return apps[game.activePlatform.storeIdentifier!].LaunchOptions || ''; + } } diff --git a/src/utils/LaunchUtils.ts b/src/utils/LaunchUtils.ts index 6079f4295..294bd1a86 100644 --- a/src/utils/LaunchUtils.ts +++ b/src/utils/LaunchUtils.ts @@ -8,6 +8,7 @@ import ManagerSettings from '../r2mm/manager/ManagerSettings'; import ModLinker from '../r2mm/manager/ModLinker'; import {Platform} from '../assets/data/ecosystemTypes'; import LinuxGameDirectoryResolver from '../r2mm/manager/linux/GameDirectoryResolver'; +import DarwinGameDirectoryResolver from "../r2mm/manager/darwin/DarwinGameDirectoryResolver"; import {LaunchType} from "../model/real_enums/launch/LaunchType"; import path from "../providers/node/path/path"; import PathResolver from "../r2mm/manager/PathResolver"; @@ -98,7 +99,7 @@ export async function isManagerRunningOnFlatpak(): Promise { export async function getProvidedWrapperArguments(game: Game): Promise { return Promise.resolve() - .then(async () => await (GameDirectoryResolverProvider.instance as LinuxGameDirectoryResolver).getLaunchArgs(game)) + .then(async () => await (GameDirectoryResolverProvider.instance as LinuxGameDirectoryResolver | DarwinGameDirectoryResolver).getLaunchArgs(game)) .then(launchArgs => { if (typeof launchArgs !== 'string') { throw launchArgs; @@ -121,16 +122,44 @@ export async function areAnyWrapperArgumentsProvided(game: Game): Promise false); } +/** + * Returns true if any old MacOS wrappers are set. These are no longer valid. + * @param game - The game to check for the set launch arguments +*/ +export async function areAnyOldMacWrapperArgumentsProvided(game: Game): Promise { + const oldWrappers = [ + 'macos_proxy', // old binary wrapper + 'macos_wrapper.sh', // old shell script wrapper + 'linux_wrapper.sh', // Linux wrapper; never officially instructed for use on Mac, but some may have tried it + ]; + return getProvidedWrapperArguments(game) + .then(launchArgs => oldWrappers.some(old => launchArgs.includes(old))) + .catch(() => false); +} + export async function areWrapperArgumentsProvided(game: Game): Promise { const isFlatpak = await isManagerRunningOnFlatpak(); const flatpakWrapper = path.join(PathResolver.MOD_ROOT, 'web_start_wrapper.sh'); const linuxWrapper = path.join(PathResolver.MOD_ROOT, 'linux_wrapper.sh'); - const appropriateWrapper = isFlatpak ? flatpakWrapper : linuxWrapper; + const appropriateWrapper = (isFlatpak || appWindow.getPlatform() === 'darwin') ? flatpakWrapper : linuxWrapper; return getProvidedWrapperArguments(game) .then(launchArgs => launchArgs.includes(appropriateWrapper)) .catch(() => false); } export async function getWrapperLaunchArgs(): Promise { - return `"${path.join(PathResolver.MOD_ROOT, appWindow.getPlatform() === 'darwin' ? 'macos_proxy' : 'linux_wrapper.sh')}" %command%`; + return `"${path.join(PathResolver.MOD_ROOT, appWindow.getPlatform() === 'darwin' ? 'web_start_wrapper.sh' : 'linux_wrapper.sh')}" %command%`; +} + +export async function ensureScriptsAreAllExecutable() { + try { + const shFiles = (await FsProvider.instance.readdir(await FsProvider.instance.realpath(Profile.getActiveProfile().getProfilePath()))) + .filter(value => value.endsWith(".sh")); + for (const shFile of shFiles) { + await FsProvider.instance.chmod(await FsProvider.instance.realpath(Profile.getActiveProfile().joinToProfilePath(shFile)), 0o755); + } + } catch (e) { + const err: Error = e as Error; + return new R2Error("Failed to make script file executable", err.message, "You may need to run the manager with elevated privileges."); + } } diff --git a/yarn.lock b/yarn.lock index 12d64114b..ed910642a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2257,7 +2257,7 @@ __metadata: languageName: node linkType: hard -"@types/plist@npm:^3.0.1": +"@types/plist@npm:^3.0.1, @types/plist@npm:^3.0.5": version: 3.0.5 resolution: "@types/plist@npm:3.0.5" dependencies: @@ -9784,6 +9784,7 @@ __metadata: "@types/jsdom": "npm:^21.1.7" "@types/lodash.debounce": "npm:^4.0.7" "@types/node": "npm:^20.19.1" + "@types/plist": "npm:^3.0.5" "@types/quill": "npm:^2.0.3" "@types/sinon": "npm:^10.0.2" "@types/unzipper": "npm:^0.10.1" @@ -9827,6 +9828,7 @@ __metadata: modern-normalize: "npm:^3.0.1" moment: "npm:^2.29.1" node-ipc: "npm:^12.0.0" + plist: "npm:^3.1.0" postcss: "npm:^8.5.6" quasar: "npm:^2.18.6" quicktype-core: "npm:^23.0.171"