diff options
Diffstat (limited to 'soibox')
-rwxr-xr-x | soibox | 629 |
1 files changed, 629 insertions, 0 deletions
@@ -0,0 +1,629 @@ +#!/bin/sh + +soibox_version="0.1" +config_dir="$HOME/.config/soibox" +store_dir="$HOME/.local/share/soibox" +cache_dir="$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 Mangohud, enable fps overlay + -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:mil:gsfdb" f + do + case "$f" in + r) + local_config="$(readlink -f "$OPTARG")" + + [ -f "$local_config" ] || + die "$local_config, no such file" + ;; + m) + mangohud=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 "$store_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 "$store_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 "${store_dir}/${1##*/}" 2> /dev/null + ln -s "${store_dir}/${1##*/}:${release_gittag}-${release_id}" "${store_dir}/${1##*/}" + dl_path="${store_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= + 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)" + + # 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 dxvk_sh + unset dl_path +} + +setup_vkd3d() +{ + # usage: setup_vkd3d "gh_username/project" [tag] + : "${1:?}" + dl_path= + vkd3d_exe= + + 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)" + + if [ "$loglevel" != 3 ] + then + "$vkd3d_exe" install > /dev/null 2>&1 + else + "$vkd3d_exe" install + fi + + unset vkd3d_exe + 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 +} + +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 "$store_dir" ] || + mkdir -p "$store_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 + + [ "$dry_run" = "true" ] && + exit + + # print banner + [ "$loglevel" != 0 ] && [ -n "$banner" ] && + print_banner "$(absolute_path "$banner" "$local_config")" + + WINEPREFIX="${store_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 ] && dep_check "mangohud" && + 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=${store_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 "$@" |