aboutsummaryrefslogblamecommitdiff
path: root/soibox
blob: 381be5b57e3a4ff3e4861e322de6a5f2567c1737 (plain) (tree)
1
2
3
4
5
6


                    


                                                      












                                                                               
                                                                               

















































































































































                                                                                                 
                                       








                                                                 
                                                
































































                                                                                                                 
                                     






































                                                                                                          
                                                                            




                                                                                                           


                                                                                                           


















































                                                                                      

                                                                             

                                                                                                                 












                                                                 



                                                                              


















                                                                     

                          









                                                        

                                                                             












                                                                          



                                                                               








                                                     

                          













































                                                                                           







































                                                                                                                                           


































                                                  

                                    













































































                                                                                                                   


                                          






                                                                         
                                       














                                                                            
                               





                                                                                                        
                                                                                                   


























                                                                                                  
#!/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 "$@"