#!/bin/sh
soibox_version="0.1"
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 <game.sb> 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 [profile] 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="${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:gsfdb" f
do
case "$f" in
r)
local_config="$(readlink -f "$OPTARG")"
[ -f "$local_config" ] ||
die "$local_config, no such file"
;;
m)
mangohud="${OPTARG:-on}"
;;
i)
ignore_globalconf=true
;;
l)
loglevel="${OPTARG:?l: loglevel cant be empty}"
;;
g)
gamemode=on
;;
s)
gamescope=on
;;
f)
firejail=on
firejail_prof="$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 -LO "$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 -LO "$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 "config, mangohud, ${1}, invalid option"
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 "$@"
[ -f "$config_dir/soibox.conf" ] && [ "$ignore_globalconf" != true ]
parse_config "$config_dir/soibox.conf"
parse_config "$local_config"
: "${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 "$@"