Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
47 changes: 36 additions & 11 deletions public/linux_wrapper.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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
Binary file removed public/macos_proxy
Binary file not shown.
45 changes: 0 additions & 45 deletions public/macos_wrapper.sh

This file was deleted.

4 changes: 2 additions & 2 deletions src/components/computed/WrapperArguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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%`;
}
Expand Down
16 changes: 14 additions & 2 deletions src/pages/LinuxNativeGameSetup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
<p>The Flatpak version of {{ appName }} now uses a different wrapper script.</p>
<p>You must update your launch arguments to support this.</p>
</div>
</div>
<div class="container margin-bottom" v-if="hasOldMacWrapperSet">
<div class="notification is-warning">
<p>It looks like you've previously set launch arguments.</p>
<p>The MacOS version of {{ appName }} now uses a different wrapper script.</p>
<p>You must update your launch arguments to support this.</p>
</div>
</div>
<div class="container">
To be able to launch {{ activeGame.displayName }} on Linux, you must first setup your Steam launch options correctly.<br/>
To be able to launch {{ activeGame.displayName }} on {{ platformName }}, you must first setup your Steam launch options correctly.<br/>
This needs to be done because of how the BepInEx injection works on Unix systems.<br/>
<br/>
Please copy and paste the following to your {{ activeGame.displayName }} launch options:<br/>
Expand All @@ -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';
Expand All @@ -48,7 +56,8 @@ const platformName = computed<string>(() => 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;
});
Expand All @@ -59,6 +68,9 @@ areAnyWrapperArgumentsProvided(activeGame.value).then(value => alreadyHadValuesS
const isFlatpak = ref<boolean>(false);
isManagerRunningOnFlatpak().then(value => isFlatpak.value = value);

const hasOldMacWrapperSet = ref<boolean>(false);
areAnyOldMacWrapperArgumentsProvided(activeGame.value).then(value => hasOldMacWrapperSet.value = value)

function copy(){
let range = document.createRange();
range.selectNode(document.getElementById('copyableArgs') as Node);
Expand Down
6 changes: 5 additions & 1 deletion src/pages/Splash.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(),
Expand Down
26 changes: 26 additions & 0 deletions src/r2mm/launching/runners/darwin/EgsGameRunner_Darwin.ts
Original file line number Diff line number Diff line change
@@ -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<string[] | R2Error> {
return this.directGameRunner.getGameArguments(game, profile);
}

public override startModded(game: Game, profile: Profile): Promise<void | R2Error> {
window.electron.openExternal('com.epicgames.launcher://');
return this.directGameRunner.startModded(game, profile);
}

public override startVanilla(game: Game, profile: Profile): Promise<void | R2Error> {
window.electron.openExternal('com.epicgames.launcher://');
return this.directGameRunner.startVanilla(game, profile);
}
}
22 changes: 17 additions & 5 deletions src/r2mm/launching/runners/darwin/SteamGameRunner_Darwin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -35,7 +38,7 @@ export default class SteamGameRunner_Darwin extends GameRunnerProvider {
async start(game: Game, args: string[]): Promise<void | R2Error> {
const settings = await ManagerSettings.getSingleton(game);
const steamDir = await GameDirectoryResolverProvider.instance.getSteamDirectory();
if(steamDir instanceof R2Error) {
if (steamDir instanceof R2Error) {
return steamDir;
}

Expand All @@ -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');
Expand Down
16 changes: 3 additions & 13 deletions src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
Loading