#!/bin/sh soibox_version="0.2" config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/soibox" data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/soibox" cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/soibox" usage() { cat <<- EOF Usage: ${0##*/} command [argument] [options] A game launcher for the command line Commands: -r Play the game specified in the game profile -h | --help Show this help cruft --version Echo the ${0##*/} version Options: -i Ignore global config -l [0-3] Loglevel, 1 being nothing, 2 being errors, and 3 all -m [1-4] Enable performance overlay, 4 being the most verbose -g Use feral's gamemode -s Use gamescope -d Dryrun, shows you errors in config -b Use BindToInterface to block wan activity -f Use firejail for sandboxing games EOF } warn() { : "${1:?}" printf "\033[33;1m---> warn: %b\033[0m\n" "$*" >&2 } note() { : "${1:?}" printf "\033[32;1m---> %b\033[0m\n" "$*" } die() { # usage: die "reason" [exit_status] : "${1:?}" printf "\033[31;1merr: %b\033[0m\n" "$1" exit "${2:-1}" } dep_check() { # usage: cmd_check cmd_1 ... : "${1:?}" for dep; do command -v "$dep" 1>/dev/null || die "$dep not found, please install it" 127 done unset dep } parse_config() { # usage: parse_config path_to_config ## TODO: gpu oem specifc conf ## TODO: vram, ram limitation, give proper reasons when the games fails to run ## TODO: gpu version specifc conf or allow if available, dlss, rtx, etc.. ## TODO: parse and use proton gamefixes localted at proton_release/protonfixes, possible? : "${1:?}" olf_ifs="$IFS" while IFS="$(printf ' :\t')" read -r key val do # skip lines containing comments, # [ -z "${key##\#*}" ] && continue : "${val:?config: can"'"t be empty}" case "$key" in mangohud) : "${mangohud:=$val}" ;; loglevel) : "${loglevel:=$val}" ;; banner) : "${banner:=$val}" ;; gamemode) : "${gamemode:=$val}" ;; gamescope) : "${gamescope:=$val}" ;; firejail) : "${firejail:=$val}" ;; bind_interface) : "${bind_interface:=$val}" ;; wine) : "${wine:=$val}" ;; winedll_ovrids) : "${winedll_ovrids:=$val}" ;; wine_tag) : "${wine_tag:=$val}" ;; wine_prefix) : "${wine_prefix:=$val}" ;; dxvk) : "${dxvk:=$val}" ;; dxvk_tag) : "${dxvk_tag:=$val}" ;; vkd3d) : "${vkd3d:=$val}" ;; vkd3d_tag) : "${vkd3d_tag:=$val}" ;; groot) : "${groot:=$val}" ;; executable) : "${executable:=$val}" ;; offline_mode) : "${offline_mode:=$val}" ;; *) warn "config, $key, invalid option" ;; esac done < "$1" IFS="$olf_ifs" unset old_ifs } parse_opts() { if [ "$1" = "--version" ] then echo "soibox $soibox_version" echo "Copyright (c) 2023 Sinan Muhammed" exit 0 elif [ "$1" = "--help" ] || [ "$1" = "-h" ] || [ "$#" -eq 0 ] then usage exit 0 elif [ -f "$1" ] then local_config="$(readlink -f "$1")" shift fi while getopts "r:m:il:gsf:bd" f do case "$f" in r) local_config="$(readlink -f "$OPTARG")" [ -f "$local_config" ] || die "$local_config, no such file" ;; m) mangohud="$OPTARG" ;; i) ignore_globalconf=true ;; l) loglevel="$OPTARG" ;; g) gamemode=on ;; s) gamescope=on ;; f) firejail="$OPTARG" ;; b) bind_interface=on ;; d) dry_run=true ;; ?) usage exit 1 ;; esac done [ "$local_config" ] || die "no local config passed" } github_dl_release() { # usage: github_dl_release "gh_username/project" [tag] : "${1:?}" release=${2:+tags/$2} : "${release:=latest}" release_link= release_id=0 release_gittag="$2" latest_id=0 fetch= offline=true dep_check curl # check github for new releases if curl -Is "https://github.com" 1> /dev/null then fetch="$(curl -s --write-out "%{http_code}" "https://api.github.com/repos/$1/releases/$release")" if [ -n "${fetch##*200}" ] then warn "non 200 http status code, fetch failed for github.com/$1" else : "${release_gittag:="$(echo "$fetch" | grep -m1 tag_name | cut -d'"' -f4)"}" release_id="$(echo "$fetch" | grep -m1 id | grep -o "[0-9]*")" offline=false fi fi # compare github releases with local cache for local_id in "$data_dir"/* do case "${local_id##*/}" in *[[:alpha:]]) # skip over symlinks continue ;; "${1##*/}":"$release_gittag"-*) dl_path="${local_id}/" return 0 ;; "${1##*/}"*) [ "$release" = "latest" ] && [ "${local_id##*-}" -gt "$latest_id" ] && latest_id="${local_id##*-}" if [ -n "${release##tags*}" ] && [ "$offline" = "true" ] then offline=noprb dl_path="${local_id}/" fi ;; esac done # download necessary releases if not available locally [ "$offline" = "true" ] && die "can't connect to github.com:443, failed to download ${1##*/}, $release" 101 if [ "$offline" = "noprb" ] then [ "$offline_mode" != on ] && warn "offline, you might be running a outdated version of ${1##*/}" else release_link="$(echo "$fetch" | grep -E -m1 -o "https://github.com/$1/releases/download/.*/.*\.tar\.([xg]z|zst)")" note "downloading ${1##*/}, ${release##*/}" curl -LOC - "$release_link" --output-dir "${3:-$cache_dir}" tar -xaf "${cache_dir}/${release_link##*/}" -C "$data_dir" \ --one-top-level="${1##*/}:${release_gittag}-${release_id}" --strip-components 1 || die "failed to extract $cache_dir/${release_link##*/}" if [ "$latest_id" -lt "$release_id" ] then rm "${data_dir}/${1##*/}" 2> /dev/null ln -s "${data_dir}/${1##*/}:${release_gittag}-${release_id}" "${data_dir}/${1##*/}" dl_path="${data_dir}/${1##*/}/" fi fi unset offline unset fetch unset latest_id unset release unset release_id unset release_gittag unset release_link } setup_wine() { # usage: setup_wine "[gh_username/project | system]" [tag] : "${1:?}" dl_path= [ "$loglevel" = 3 ] && note "setting up wine" if [ "$1" = "system" ] then dep_check "wine" "wineboot" wine_exe="$(command -v wine)" return elif [ "$2" = "latest" ]; then github_dl_release "$1" wine_exe="$(find "$dl_path" -type f -name wine | grep -m1 "bin/wine")" else github_dl_release "$1" "$2" wine_exe="$(find "$dl_path" -type f -name wine | grep -m1 "bin/wine")" fi [ -n "$WINEPREFIX" ] && ! [ -f "$WINEPREFIX/system.reg" ] && if [ "$loglevel" != 3 ] then "${wine_exe%wine}wineboot" -u > /dev/null 2>&1 else "${wine_exe%wine}wineboot" -u fi unset dl_path } setup_dxvk() { # usage: setup_dxvk "gh_username/project" [tag] : "${1:?}" dl_path= dxvk_exe= d3d11_dxvk= d3d11_prefix="${WINEPREFIX}/dosdevices/c:/windows/system32/d3d11.dll" dxvk_sh="https://gist.githubusercontent.com/doitsujin/1652e0e3382f0e0ff611e70142684d01/raw/setup_dxvk.sh" export WINE_LARGE_ADDRESS_AWARE=1 dep_check "wine" [ "$loglevel" = 3 ] && note "setting up dxvk" if [ -z "$2" ] || [ "$2" = "latest" ] then github_dl_release "$1" else github_dl_release "$1" "$2" fi dxvk_exe="$(find "$dl_path" -type f -name setup_dxvk.sh)" d3d11_dxvk="$(find "$dl_path" -type f -name d3d11.dll | grep -m1 x64)" diff "$d3d11_dxvk" "$d3d11_prefix" > /dev/null 2>&1 && return # author removed setup_dxvk.sh in v2.1 if [ -z "$dxvk_exe" ] then note "downloading dxvk setup" curl -LOC - "$dxvk_sh" --output-dir "$dl_path" || die "failed to download setup_dxvk.sh" chmod +x "${dl_path}/setup_dxvk.sh" dxvk_exe="${dl_path}/setup_dxvk.sh" fi if [ "$loglevel" != 3 ] then "$dxvk_exe" "${dxvk_exe%/*}" install > /dev/null 2>&1 else "$dxvk_exe" "${dxvk_exe%/*}" install fi unset dxvk_exe unset d3d11_dxvk unset d3d11_prefix unset dxvk_sh unset dl_path } setup_vkd3d() { # usage: setup_vkd3d "gh_username/project" [tag] : "${1:?}" dl_path= vkd3d_exe= d3d12_vkd3d= d3d12_prefix="${WINEPREFIX}/dosdevices/c:/windows/system32/d3d12.dll" dep_check "wine" [ "$loglevel" = 3 ] && note "setting up vkd3d" if [ -z "$2" ] || [ "$2" = "latest" ] then github_dl_release "$1" else github_dl_release "$1" "$2" fi vkd3d_exe="$(find "$dl_path" -type f -name setup_vkd3d_proton.sh)" d3d12_vkd3d="$(find "$dl_path" -type f -name d3d12.dll | grep -m1 x64)" diff "$d3d12_vkd3d" "$d3d12_prefix" > /dev/null 2>&1 && return if [ "$loglevel" != 3 ] then "$vkd3d_exe" install > /dev/null 2>&1 else "$vkd3d_exe" install fi unset vkd3d_exe unset d3d12_prefix unset d3d12_vkd3d unset dl_path } print_banner() { : "${1:?}" if [ "${1%%*.gz}" ] then cat "$1" else zcat "$1" fi } setup_groot() { if [ -z "${1%%*.tar.zst}" ] || [ -z "${1%%*.tar.xz}" ] || [ -z "${1%%*.tar}" ] || [ -z "${1%%*.tar.gz}" ] then groot="${1%/*}/groot" [ -d "$groot" ] && return note "extracting ${1##*/}" dep_check "tar" tar -xaf "$1" -C "${1%/*}" --one-top-level="groot" --strip-components 1 || die "failed to extract $1" elif [ -z "${1%%*.dwarfs}" ]; then # TODO: support mounting dwarfs archives groot="${1%/*}/groot" [ -d "$groot" ] && return note "extracting ${1##*/}" dep_check "dwarfs" "dwarfsextract" mkdir -p "${1%/*}/groot" dwarfsextract -i "$1" -o "${1%/*}/groot" > /dev/null || die "failed to extract $1" fi } setup_mangohud() { : "${1:?setup_mangohud: empty \$1}" MANGOHUD_CONFIG= mangohud=on dep_check "mangohud" case "$1" in 1) MANGOHUD_CONFIG="frame_timing=0,cpu_stats=0,gpu_stats=0,fps=1,fps_only,legacy_layout=0,width=40,frametime=0" ;; 2) MANGOHUD_CONFIG="battery" ;; 3) MANGOHUD_CONFIG="cpu_temp,gpu_temp,ram,vram,io_read,io_write,arch,gpu_name,cpu_power,gpu_power,wine,frametime,battery" ;; 4) MANGOHUD_CONFIG="full,cpu_temp,gpu_temp,ram,vram,io_read,io_write,arch,gpu_name,cpu_power,gpu_power,wine,frametime,battery" ;; on) # use the default config ;; *) if [ -f "$(absolute_path "$mangohud" "$local_config" )" ] then MANGOHUD_CONFIGFILE="$(absolute_path "$mangohud" "$local_config" )" export MANGOHUD_CONFIGFILE else warn "mangohud, ${1}, invalid value" mangohud=off fi ;; esac [ -n "$MANGOHUD_CONFIG" ] && export MANGOHUD_CONFIG } absolute_path() { # usage: absolute_path "path" "local_conf" : "${1:?absolute_path: empty \$1}" : "${2:?absolute_path: empty \$2}" rltv_path= abs_path= # solve local_config path if [ -z "${2##*/*}" ] then rltv_path="${2%/*}" else rltv_path="$PWD" fi [ -z "${1##/*}" ] && abs_path="$1" [ -z "${1##\~/*}" ] && abs_path="${HOME}/${1##\~/}" [ -z "$abs_path" ] && abs_path="${rltv_path}/${1}" echo "$abs_path" unset abs_path unset rltv_path } main() { [ "$(id -u)" -eq 0 ] && die "dont run soibox as root" [ -d "$config_dir" ] || mkdir -p "$config_dir" [ -d "$data_dir" ] || mkdir -p "$data_dir" [ -d "$cache_dir" ] || mkdir -p "$cache_dir" # soibox vars cmd= wine_exe= dry_run= firejail_prof= # user vars ignore_globalconf= local_config= mangohud= loglevel= banner= gamemode= gamescope= firejail= wine= wine_tag= wine_prefix= dxvk= dxvk_tag= vkd3d= vkd3d_tag= groot= executable= offline_mode= winedll_ovrids= bind_interface= # parse input parse_opts "$@" parse_config "$local_config" [ -f "$config_dir/soibox.conf" ] && [ "$ignore_globalconf" != true ] && parse_config "$config_dir/soibox.conf" [ -f "/etc/soibox/soibox.conf" ] && [ "$ignore_globalconf" != true ] && parse_config "/etc/soibox/soibox.conf" : "${loglevel:=1}" # validate input if [ -n "$firejail" ] && [ "$firejail" != on ] then [ -f "$(absolute_path "$firejail" "$local_config")" ] && firejail_prof="$(absolute_path "$firejail" "$local_config")" [ -f "${HOME}/.config/firejail/${firejail}.profile" ] && firejail_prof="${HOME}/.config/firejail/${firejail}.profile" [ -f "/etc/firejail/${firejail}.profile" ] && firejail_prof="/etc/firejail/${firejail}.profile" if [ -n "$firejail_prof" ] then firejail=on else die "unable to locate firejail profile" fi fi if [ "$firejail" = on ] && [ "$bind_interface" = on ] then warn "disabling bind to interface, not needed when using firejail" bind_interface=off fi if [ "$bind_interface" = on ] then [ -f "/usr/lib/bindToInterface.so" ] || die "unable to locate /usr/lib/bindToInterface.so" export BIND_INTERFACE=lo BIND_EXCLUDE=10.,172.16.,192.168. LD_PRELOAD="/usr/lib/bindToInterface.so" fi groot="$(absolute_path "$groot" "$local_config")" setup_groot "$groot" if [ -f "${groot}/${executable}" ] then cmd="${groot}/${executable}" else die "unable to locate the game executable" fi [ -n "$mangohud" ] && setup_mangohud "$mangohud" [ "$dry_run" = "true" ] && exit # print banner [ "$loglevel" != 0 ] && [ -n "$banner" ] && print_banner "$(absolute_path "$banner" "$local_config")" WINEPREFIX="${data_dir}/prefix" [ -n "$wine_prefix" ] && WINEPREFIX="$(absolute_path "$wine_prefix" "$local_config")" export WINEPREFIX [ -n "$winedll_ovrids" ] && export WINEDLLOVERRIDES="$winedll_ovrids" [ -n "$wine" ] && setup_wine "$wine" "$wine_tag" [ -n "$dxvk" ] && setup_dxvk "$dxvk" "$dxvk_tag" [ -n "$vkd3d" ] && setup_vkd3d "$vkd3d" "$vkd3d_tag" [ -n "$wine" ] && cmd="${wine_exe} ${cmd}" [ "$mangohud" = on ] && cmd="mangohud ${cmd}" [ "$gamemode" = on ] && dep_check "gamemoderun" && cmd="gamemoderun ${cmd}" ## gamemode wont work without --ignore=noroot, https://github.com/netblue30/firejail/issues/5035 if [ "$firejail" = on ] && dep_check "firejail" then cmd="--whitelist=${WINEPREFIX} --whitelist=${data_dir} --whitelist=${groot} ${cmd}" cmd="firejail --net=none --ignore=noroot --profile=${firejail_prof:-steam} ${cmd}" fi ## gamemode wont recognize gamescope, but it's necessary to work with firejail ## TODO: https://github.com/netblue30/firejail/issues/5587 [ "$gamescope" = on ] && dep_check "gamescope" && cmd="gamescope -f -- ${cmd}" # wait for system wine called by vkd3d and dxvk [ "$loglevel" = 3 ] && note "waiting for vkd3d and dxvk" while grep --text "$WINEPREFIX" /proc/"$(pgrep wineserver)"/environ > /dev/null 2>&1 do sleep .5 done # start the game [ "$loglevel" -le 1 ] && exec > /dev/null 2>&1 [ "$loglevel" = 2 ] && exec > /dev/null [ "$loglevel" = 3 ] && note "starting the game" cd "$groot" && eval "$cmd" } main "$@"