#!/bin/bash
set -o pipefail
version=3.1.0-rc.16
aid=221100
game="dayz"
workshop="steam://url/CommunityFilePage/"
api="https://api.battlemetrics.com/servers"
sd_res="--width=1280 --height=800"
config_path="$HOME/.config/dztui/"
config_file="${config_path}dztuirc"
hist_file="${config_path}/history"
tmp=/tmp/dzgui.tmp
fifo=/tmp/table.tmp
separator="%%"
check_config_msg="Check config values and restart."
issues_url="https://github.com/aclist/dztui/issues"
url_prefix="https://raw.githubusercontent.com/aclist/dztui"
stable_url="$url_prefix/dzgui"
testing_url="$url_prefix/testing"
releases_url="https://github.com/aclist/dztui/releases/download/browser"
help_url="https://aclist.github.io/dzgui/dzgui"
freedesktop_path="$HOME/.local/share/applications"
sd_install_path="$HOME/.local/share/dzgui"
helpers_path="$sd_install_path/helpers"
geo_file="$helpers_path/ips.csv"
km_helper="$helpers_path/latlon"
sums_path="$helpers_path/sums.md5"
scmd_file="$helpers_path/scmd.sh"
km_helper_url="$releases_url/latlon"
db_file="$releases_url/ips.csv.gz"
sums_url="$testing_url/helpers/sums.md5"
scmd_url="$testing_url/helpers/scmd.sh"
vdf2json_url="$testing_url/helpers/vdf2json.py"
notify_url="$testing_url/helpers/d.html"
notify_img_url="$testing_url/helpers/d.webp"
forum_url="https://github.com/aclist/dztui/discussions"
version_file="$config_path/versions"
#TODO: prevent connecting to offline servers
#TODO: abstract zenity title params and dimensions
update_last_seen(){
mv $config_file ${config_path}dztuirc.old
nr=$(awk '/seen_news=/ {print NR}' ${config_path}dztuirc.old)
seen_news="seen_news=\"$sum\""
awk -v "var=$seen_news" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > $config_file
printf "[DZGUI] Updated last seen news item to '$sum'\n"
source $config_file
}
check_news(){
[[ $branch == "stable" ]] && news_url="$stable_url/news"
[[ $branch == "testing" ]] && news_url="$testing_url/news"
result=$(curl -Ls "$news_url")
sum=$(echo -n "$result" | md5sum | awk '{print $1}')
}
print_news(){
check_news
if [[ $sum == $seen_news || -z $result ]]; then
hchar=""
news=""
else
hchar="─"
news="$result\n$(awk -v var="$hchar" 'BEGIN{for(c=0;c<90;c++) printf var;}')\n"
update_last_seen
fi
}
declare -A deps
deps=([awk]="5.1.1" [curl]="7.80.0" [jq]="1.6" [tr]="9.0" [zenity]="3.42.1" [steam]="1.0.0" [python]="3.0")
changelog(){
if [[ $branch == "stable" ]]; then
md="https://raw.githubusercontent.com/aclist/dztui/dzgui/changelog.md"
else
md="https://raw.githubusercontent.com/aclist/dztui/testing/changelog.md"
fi
prefix="This window can be scrolled."
echo $prefix
echo ""
curl -Ls "$md" | awk '/Unreleased/ {flag=1}flag'
}
depcheck(){
for dep in "${!deps[@]}"; do
command -v $dep 2>&1>/dev/null || (printf "Requires %s >=%s\n" $dep ${deps[$dep]}; exit 1)
done
}
watcher_deps(){
if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then
echo "100"
warn "Missing dependency: requires 'wmctrl' or 'xdotool'.\nInstall from your system's package manager."
exit 1
fi
}
init_items(){
#array order determines menu selector; this is destructive
items=(
"[Connect]"
" Server browser"
" My servers"
" Quick connect to favorite server"
" Connect by IP"
" Recent servers (last 10)"
"[Manage servers]"
" Add server by ID"
" Add favorite server"
" Delete server"
"[Options]"
" List installed mods"
" View changelog"
" Advanced options"
"[Help]"
" Help file ⧉"
" Report bug ⧉"
" Forum ⧉"
)
}
warn_and_exit(){
zenity --info --title="DZGUI" --text="$1" --width=500 --icon-name="dialog-warning" 2>/dev/null
printf "[DZGUI] %s\n" "$check_config_msg"
exit
}
warn(){
zenity --info --title="DZGUI" --text="$1" --width=500 --icon-name="dialog-warning" 2>/dev/null
}
info(){
zenity --info --title="DZGUI" --text="$1" --width=500 2>/dev/null
}
set_api_params(){
response=$(curl -s "$api" -H "Authorization: Bearer "$api_key"" -G -d "sort=-players" \
-d "filter[game]=$game" -d "filter[ids][whitelist]=$list_of_ids")
list_response=$response
first_entry=1
}
query_api(){
#TODO: prevent drawing list if null values returned without API error
if [[ $one_shot_launch -eq 1 ]]; then
list_of_ids="$fav"
else
list_of_ids="$whitelist"
fi
set_api_params
if [[ "$(jq -r 'keys[]' <<< "$response")" == "errors" ]]; then
code=$(jq -r '.errors[] .status' <<< $response)
#TODO: fix granular api codes
if [[ $code -eq 401 ]]; then
warn "Error $code: malformed API key"
return
elif [[ $code -eq 500 ]]; then
warn "Error $code: malformed server list"
return
fi
fi
if [[ -z $(echo $response | jq '.data[]') ]]; then
warn "95: API returned empty response. Check config file."
return
fi
}
write_config(){
cat <<-END
#Path to DayZ installation
steam_path="$steam_path"
#Your unique API key
api_key="$api_key"
#Comma-separated list of server IDs
whitelist="$whitelist"
#Favorite server to fast-connect to (limit one)
fav="$fav"
#Custom player name (optional, required by some servers)
name="$name"
#Set to 1 to perform dry-run and print launch options
debug="$debug"
#Toggle stable/testing branch
branch="$branch"
#Last seen news item
seen_news="$seen_news"
#Steam API key
steam_api="$steam_api"
#Terminal emulator
term="$term"
#Auto-install mods
auto_install="$auto_install"
#Automod staging directory
staging_dir="$staging_dir"
#Path to default Steam client
default_steam_path="$default_steam_path"
END
}
write_desktop_file(){
cat <<-END
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Exec=$sd_install_path/dzgui.sh
Name=DZGUI
Comment=dzgui
Icon=$sd_install_path/dzgui
Categories=Game
END
}
freedesktop_dirs(){
mkdir -p "$sd_install_path"
mkdir -p "$freedesktop_path"
#TODO: update url
curl -s "$version_url" > "$sd_install_path/dzgui.sh"
chmod +x "$sd_install_path/dzgui.sh"
img_url="https://raw.githubusercontent.com/aclist/dztui/testing/images"
for i in dzgui grid.png hero.png logo.png; do
curl -s "$img_url/$i" > "$sd_install_path/$i"
done
write_desktop_file > "$freedesktop_path/dzgui.desktop"
if [[ $is_steam_deck -eq 1 ]]; then
write_desktop_file > "$HOME/Desktop/dzgui.desktop"
fi
}
find_library_folder(){
steam_path=$(python $helpers_path/vdf2json.py -i $default_steam_path/steamapps/libraryfolders.vdf | jq -r '.libraryfolders[]|select(.apps|has("221100")).path')
}
file_picker(){
while true; do
local path=$(zenity --file-selection --directory 2>/dev/null)
if [[ -z "$path" ]]; then
return
else
default_steam_path="$path"
find_library_folder
fi
if [[ -z $steam_path ]]; then
warn "DayZ not found at this path."
else
return
fi
done
}
create_config(){
while true; do
player_input="$(zenity --forms --add-entry="Player name (required for some servers)" --add-entry="BattleMetrics API key" --add-entry="Steam API key" --title="DZGUI" --text="DZGUI" $sd_res --separator="│" 2>/dev/null)"
#explicitly setting IFS crashes zenity in loop
#and mapfile does not support high ascii delimiters
#so split fields with newline
readarray -t args < <(echo "$player_input" | sed 's/│/\n/g')
name="${args[0]}"
api_key="${args[1]}"
steam_api="${args[2]}"
[[ -z $player_input ]] && exit
if [[ -z $api_key ]] || [[ -z $steam_api ]]; then
warn "API key: invalid value"
#TODO: test BM key
elif [[ $(test_steam_api) -eq 1 ]]; then
warn "Invalid Steam API key"
else
while true; do
find_default_path
find_library_folder
if [[ -z $steam_path ]]; then
return
else
mkdir -p $config_path
write_config > $config_file
info "Config file created at $config_file."
source $config_file
return
fi
done
fi
done
}
err(){
printf "[ERROR] %s\n" "$1"
}
varcheck(){
[[ -z $api_key ]] && (err "Error in key: 'api_key'")
[[ ! -d "$game_dir" ]] && (err "Malformed game path")
}
run_depcheck(){
if [[ -z $(depcheck) ]]; then
:
else
echo "100"
zenity --warning --ok-label="Exit" --title="DZGUI" --text="$(depcheck)"
exit
fi
}
run_varcheck(){
source $config_file
workshop_dir="$steam_path/steamapps/workshop/content/$aid"
game_dir="$steam_path/steamapps/common/DayZ"
if [[ -z $(varcheck) ]]; then
:
else
zenity --warning --width 500 --text="$(varcheck)" 2>/dev/null
printf "[DZGUI] %s\n" "$check_config_msg"
zenity --question --cancel-label="Exit" --text="Malformed config file. This is probably user error.\nStart first-time setup process again?" --width=500 2>/dev/null
code=$?
if [[ $code -eq 1 ]]; then
exit
else
#echo "100"
create_config
fi
fi
}
config(){
if [[ ! -f $config_file ]]; then
#echo "100"
zenity --width 500 --info --text="Config file not found. Click OK to proceed to first-time setup." 2>/dev/null
code=$?
#prevent progress if user hits ESC
if [[ $code -eq 1 ]]; then
exit
else
create_config
fi
else
source $config_file
fi
}
open_mod_links(){
link_file=$(mktemp)
echo "" > $link_file
echo "
DZGUI" >> $link_file
echo "DZGUI
" >> $link_file
echo "Open these links and subscribe to them on the Steam Workshop, then continue with the application prompts.
Note: it may take some time for mods to synchronize before DZGUI can see them.
It can help to have Steam in an adjacent window so that you can see the downloads completing.
" >> $link_file
n=1
for i in $diff; do
echo "$n. ${workshop}$i
"
let n++
done >> $link_file
echo "" >> $link_file
browser "$link_file" 2>/dev/null &
}
steam_deck_mods(){
until [[ -z $diff ]]; do
next=$(echo -e "$diff" | head -n1)
zenity --question --ok-label="Open" --cancel-label="Cancel" --title="DZGUI" --text="Missing mods. Click [Open] to open mod $next in Steam Workshop and subscribe to it by clicking the green Subscribe button. After the mod is downloaded, return to this menu to continue validation." --width=500 2>/dev/null
rc=$?
if [[ $rc -eq 0 ]]; then
echo "[DZGUI] Opening ${workshop}$next"
steam steam://url/CommunityFilePage/$next 2>/dev/null &
zenity --info --title="DZGUI" --ok-label="Next" --text="Click [Next] to continue mod check." --width=500 2>/dev/null
else
return 1
fi
compare
done
}
calc_mod_sizes(){
for i in "$diff"; do
local mods+=$(grep -w "$i" /tmp/modsizes | awk '{print $1}')
done
totalmodsize=$(echo -e "${mods[@]}" | awk '{s+=$1}END{print s}')
}
test_display_mode(){
pgrep -a gamescope | grep -q "generate-drm-mode"
[[ $? -eq 0 ]] && gamemode=1
}
foreground(){
if [[ $(command -v wmctrl) ]]; then
wmctrl -a "DZG Watcher"
else
local window_id=$(xdotool search --name "DZG Watcher")
xdotool windowactivate $window_id
fi
}
manual_mod_install(){
[[ $is_steam_deck -eq 1 ]] && test_display_mode
if [[ $gamemode -eq 1 ]]; then
steam_deck_mods
else
local ex="/tmp/dzc.tmp"
[[ -f $ex ]] && rm $ex
watcher(){
readarray -t stage_mods <<< "$diff"
for((i=0;i<${#stage_mods[@]};i++)); do
[[ -f $ex ]] && return 1
local downloads_dir="$steam_path/steamapps/workshop/downloads/$aid"
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
steam "steam://url/CommunityFilePage/${stage_mods[$i]}"
echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress after subscribing, try unsubscribing and resubscribing again until the download commences."
sleep 1s
foreground
until [[ -d $downloads_dir/${stage_mods[$i]} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
if [[ -d $workshop_dir/${stage_mods[$i]} ]]; then
break
fi
done
foreground
echo "# Steam is downloading ${stage_mods[$i]} (mod $((i+1)) of ${#stage_mods[@]})"
until [[ -d $workshop_dir/${stage_mods[$i]} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
done
foreground
echo "# ${stage_mods[$i]} moved to mods dir"
done
echo "100"
}
watcher > >(zenity --pulsate --progress --auto-close --title="DZG Watcher" --width=500 2>/dev/null; rc=$?; [[ $rc -eq 1 ]] && touch $ex)
compare
if [[ -z $diff ]]; then
passed_mod_check > >(zenity --pulsate --progress --auto-close --width=500 2>/dev/null)
launch
else
return 1
fi
fi
}
encode(){
echo "$1" | md5sum | cut -c -8
}
stale_symlinks(){
for l in $(find "$game_dir" -xtype l); do
unlink $l
done
}
legacy_symlinks(){
for d in "$game_dir"/*; do
if [[ $d =~ @[0-9]+-.+ ]]; then
unlink "$d"
fi
done
for d in "$workshop_dir"/*; do
local id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}')
local encoded_id=$(echo "$id" | awk '{printf("%c",$1)}' | base64 | sed 's/\//_/g; s/=//g; s/+/]/g')
if [[ -h "$game_dir/@$encoded_id" ]]; then
unlink "$game_dir/@$encoded_id"
fi
done
}
symlinks(){
for d in "$workshop_dir"/*; do
id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}')
encoded_id=$(encode "$id")
mod=$(awk -F\" '/name/ {print $2}' "$d"/meta.cpp | sed -E 's/[^[:alpha:]0-9]+/_/g; s/^_|_$//g')
link="@$encoded_id"
if [[ -h "$game_dir/$link" ]]; then
:
else
printf "[DZGUI] Creating symlink for $mod\n"
ln -fs "$d" "$game_dir/$link"
fi
done
}
passed_mod_check(){
echo "[DZGUI] Passed mod check"
echo "# Preparing symlinks"
legacy_symlinks
symlinks
echo "100"
}
auto_mod_install(){
popup 300
rc=$?
if [[ $rc -eq 0 ]]; then
calc_mod_sizes
local total_size=$(numfmt --to=iec $totalmodsize)
log="$default_steam_path/logs/content_log.txt"
[[ -f "/tmp/dz.status" ]] && rm "/tmp/dz.status"
touch "/tmp/dz.status"
console_dl "$diff" &&
steam steam://open/downloads && 2>/dev/null 1>&2
until [[ -z $(comm -23 <(printf "%s\n" "${modids[@]}" | sort) <(ls -1 $workshop_dir | sort)) ]]; do
win=$(xdotool search --name "DZG Watcher")
[[ ! $(xdotool getwindowfocus) -eq $win ]] && xdotool windowraise $win
local missing=$(comm -23 <(printf "%s\n" "${modids[@]}" | sort) <(ls -1 $workshop_dir | sort) | wc -l)
echo "# Downloaded $((${#modids[@]}-missing)) of ${#modids[@]} mods"
done | zenity --pulsate --progress --title="DZG Watcher" --auto-close --no-cancel --width=500 2>/dev/null
compare
[[ $force_update -eq 1 ]] && { unset force_update; return; }
if [[ -z $diff ]]; then
check_timestamps
passed_mod_check > >(zenity --pulsate --progress --title=DZGUI --auto-close --width=500 2>/dev/null)
launch
else
manual_mod_install
fi
else
manual_mod_install
fi
}
get_local_stamps(){
concat(){
for ((i=0;i<$max;i++)); do
echo "publishedfileids[$i]=${local_modlist[$i]}&"
done | awk '{print}' ORS=''
}
payload(){
echo -e "itemcount=${max}&$(concat)"
}
post(){
curl -s -X POST -H "Content-Type:application/x-www-form-urlencoded" \
-d "$(payload)" 'https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json'
}
post
}
update_stamps(){
for((i=0;i<${#local_modlist[@]};i++)); do
mod=${local_modlist[$i]}
stamp=${stamps[$i]}
printf "%s\t%s\n" "$mod" "$stamp" >> $version_file
done
}
check_timestamps(){
readarray -t local_modlist < <(ls -1 $workshop_dir)
max=${#local_modlist[@]}
readarray -t stamps < <(get_local_stamps | jq -r '.response.publishedfiledetails[].time_updated')
if [[ ! -f $version_file ]]; then
update_stamps
return
else
needs_update=()
for((i=0;i<${#local_modlist[@]};i++)); do
mod=${local_modlist[$i]}
stamp=${stamps[$i]}
if [[ ! $(awk -v var=$mod '$1 == var' $version_file) ]]; then
echo -e "$mod\t$stamp" >> $version_file
elif [[ $(awk -v var=$mod -v var2=$stamp '$1 == var && $2 == var2' $version_file) ]]; then
:
else
awk -v var=$mod -v var2=$stamp '$1 == var {$2=var2;print $1"\t"$2; next;};{print}' $version_file > $version_file.new
mv $version_file.new $version_file
needs_update+=($mod)
fi
done
fi
}
merge_modlists(){
[[ $force_update -eq 1 ]] && echo "# Checking mod versions"
check_timestamps
if [[ -z "$diff" ]] && [[ ${#needs_update[@]} -gt 0 ]]; then
diff=$(printf "%s\n" "${needs_update[@]}")
elif [[ -z "$diff" ]] && [[ ${#needs_update[@]} -eq 0 ]]; then
diff=
elif [[ -n "$diff" ]] && [[ ${#needs_update[@]} -eq 0 ]]; then
:
else
diff="$(echo -e "$diff\n${needs_update[@]}")"
fi
[[ $force_update -eq 1 ]] && echo "100"
}
update_history(){
[[ -n $(grep "$ip" $hist_file) ]] && return
if [[ -f $hist_file ]]; then
old=$(tail -n9 "$hist_file")
old="$old\n"
fi
echo -e "${old}${ip}" > "$hist_file"
}
is_steam_running(){
xdotool search --desktop "$(xdotool get_desktop)" --name "Steam"
}
connect(){
#TODO: sanitize/validate input
readarray -t qport_arr <<< "$qport_list"
if [[ -z ${qport_arr[@]} ]]; then
err "98: Failed to obtain query ports"
return
fi
ip=$(echo "$1" | awk -F"$separator" '{print $1}')
bid=$(echo "$1" | awk -F"$separator" '{print $2}')
if [[ $2 == "ip" ]]; then
fetch_mods_sa "$ip" > >(zenity --pulsate --progress --auto-close --no-cancel --width=500 2>/dev/null)
else
fetch_mods "$bid"
fi
if [[ $ret -eq 96 ]]; then
unset ret
return
fi
validate_mods
rc=$?
[[ $rc -eq 1 ]] && return
compare
[[ $auto_install -eq 2 ]] && merge_modlists
if [[ -n $diff ]]; then
[[ -z $(is_steam_running) ]] && { zenity --info --text "Steam must be running on the current desktop to use this feature."; return; }
if [[ $auto_install -eq 1 ]]; then
headless_mod_install "$diff"
rc=$?
[[ $rc -eq 1 ]] && manual_mod_install
elif [[ $auto_install -eq 2 ]]; then
auto_mod_install
else
manual_mod_install
fi
else
passed_mod_check > >(zenity --pulsate --progress --auto-close --width=500 2>/dev/null)
update_history
launch
fi
}
fetch_mods(){
remote_mods=$(curl -s "$api" -H "Authorization: Bearer "$api_key"" -G -d filter[ids][whitelist]="$1" -d "sort=-players" \
| jq -r '.data[] .attributes .details .modIds[]')
}
fetch_mods_sa(){
sa_ip=$(echo "$1" | awk -F: '{print $1}')
for i in ${qport_arr[@]}; do
if [[ -n $(echo "$i" | awk -v ip=$ip '$0 ~ ip') ]]; then
sa_port=$(echo $i | awk -v ip=$ip -F$separator '$0 ~ ip {print $2}')
fi
done
echo "[DZGUI] Querying modlist on ${sa_ip}:${sa_port}"
echo "# Querying modlist on ${sa_ip}:${sa_port}"
local response=$(curl -Ls "https://dayzsalauncher.com/api/v1/query/$sa_ip/$sa_port")
local status=$(echo "$response" | jq '.status')
if [[ $status -eq 1 ]]; then
echo "100"
err "97: Failed to fetch modlist"
zenity --error --title="DZGUI" --width=500 --text="[ERROR] 97: Failed to fetch modlist" 2>/dev/null &&
ret=96
return
fi
remote_mods=$(echo "$response" | jq -r '.result.mods[].steamWorkshopId')
qport_arr=()
}
prepare_ip_list(){
ct=$(< "$1" jq '[.response.servers[]]|length')
for((i=0;i<$ct;i++));do
name=$(< $json jq --arg i $i -r '[.servers[]][($i|tonumber)].name')
addr=$(< $json jq --arg i $i -r '[.servers[]][($i|tonumber)].addr')
ip=$(echo "$addr" | awk -F: '{print $1}')
players=$(< $json jq --arg i $i -r '[.servers[]][($i|tonumber)].players')
max_players=$(< $json jq --arg i $i -r '[.servers[]][($i|tonumber)].max_players')
gameport=$(< $json jq --arg i $i -r '[.servers[]][($i|tonumber)].gameport')
ip_port=$(echo "$ip:$gameport")
time=$(< $json jq --arg i $i -r '[.servers[]][($i|tonumber)].gametype' | grep -oP '(? $meta_file
json=$(mktemp)
< $meta_file jq '.response' > $json
res=$(< $meta_file jq -er '.response.servers[]' 2>/dev/null)
prepare_ip_list "$meta_file" >> /tmp/dz.hist
sleep 0.5s
done | zenity --pulsate --progress --auto-close --title=DZGUI --width=500 --no-cancel 2>/dev/null
[[ $? -eq 1 ]] && return
while true; do
sel=$(cat /tmp/dz.hist | zenity --width 1200 --height 800 --title=DZGUI --text="Recent servers" --list --column=Name --column=IP --column=Players --column=Gametime --print-column=2 --separator=%% 2>/dev/null)
if [[ $? -eq 1 ]]; then
return_from_table=1
rm /tmp/dz.hist
return
fi
if [[ -z $sel ]]; then
echo "No selection"
else
local gameport="$(echo "$sel" | awk -F: '{print $2}')"
local ip="$(echo "$sel" | awk -F: '{print $1}')"
local addr=$(< $json jq -r --arg gameport $gameport '.servers[]|select(.gameport == ($gameport|tonumber)).addr')
local qport=$(echo "$addr" | awk -F: '{print $2}')
local sa_ip=$(echo "$ip:$gameport%%$qport")
qport_list="$sa_ip"
connect "$sel" "ip"
fi
done
rm /tmp/dz.hist
}
ip_table(){
while true; do
sel=$(prepare_ip_list "$meta_file" | zenity --width 1200 --height 800 --list --column=Name --column=IP --column=Players --column=Gametime --print-column=2 --separator=%% 2>/dev/null)
if [[ $? -eq 1 ]]; then
return_from_table=1
return
fi
if [[ -z $sel ]]; then
echo "No selection"
else
local gameport="$(echo "$sel" | awk -F: '{print $2}')"
local ip="$(echo "$sel" | awk -F: '{print $1}')"
local addr=$(< $json jq -r --arg gameport $gameport '.servers[]|select(.gameport == ($gameport|tonumber)).addr')
local qport=$(echo "$addr" | awk -F: '{print $2}')
local sa_ip=$(echo "$ip:$gameport%%$qport")
qport_list="$sa_ip"
connect "$sel" "ip"
fi
done
}
fetch_ip_metadata(){
local meta_file=$(mktemp)
source $config_file
url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100\gameaddr\\$ip&key=$steam_api"
curl -Ls "$url" > $meta_file
json=$(mktemp)
< $meta_file jq '.response' > $json
res=$(< $meta_file jq -er '.response.servers[]' 2>/dev/null)
if [[ ! $? -eq 0 ]]; then
warn "[ERROR] 96: Failed to retrieve IP metadata. Check IP or API key and try again."
echo "[DZGUI] 96: Failed to retrieve IP metadata"
else
ip_table
fi
}
#TODO: local servers
#local_ip(){
#(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)
#}
test_steam_api(){
local code=$(curl -ILs "https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100&limit=10&key=$steam_api" \
| grep -E "^HTTP")
[[ $code =~ 403 ]] && echo 1
[[ $code =~ 200 ]] && echo 0
}
add_steam_api(){
[[ $(test_steam_api) -eq 1 ]] && return 1
mv $config_file ${config_path}dztuirc.old
nr=$(awk '/steam_api=/ {print NR}' ${config_path}dztuirc.old)
steam_api="steam_api=\"$steam_api\""
awk -v "var=$steam_api" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > ${config_path}dztuirc
echo "[DZGUI] Added Steam API key"
zenity --info --title="DZGUI" --text="Added Steam API key to:\n\n${config_path}dztuirc\nIf errors occur, you can restore the file:\n${config_path}dztuirc.old" --width=500 2>/dev/null
source $config_file
}
check_steam_api(){
if [[ -z $steam_api ]]; then
steam_api=$(zenity --entry --text="Key 'steam_api' not present in config file. Enter Steam API key:" --title="DZGUI" 2>/dev/null)
if [[ $? -eq 1 ]] ; then
return
elif [[ ${#steam_api} -lt 32 ]] || [[ $(test_steam_api) -eq 1 ]]; then
zenity --warning --title="DZGUI" --text="Check API key and try again." 2>/dev/null
return 1
else
add_steam_api
fi
fi
}
validate_ip(){
echo "$1" | grep -qP '^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$'
}
connect_by_ip(){
source $config_file
check_steam_api
[[ $? -eq 1 ]] && return
while true; do
if [[ $return_from_table -eq 1 ]]; then
return_from_table=0
return
fi
ip=$(zenity --entry --text="Enter server IP (omit port)" --title="DZGUI" 2>/dev/null)
[[ $? -eq 1 ]] && return
if validate_ip "$ip"; then
fetch_ip_metadata
else
continue
fi
done
}
fetch_mods(){
remote_mods=$(curl -s "$api" -H "Authorization: Bearer "$api_key"" -G -d filter[ids][whitelist]="$1" -d "sort=-players" \
| jq -r '.data[] .attributes .details .modIds[]')
}
query_defunct(){
max=${#modlist[@]}
concat(){
for ((i=0;i<$max;i++)); do
echo "publishedfileids[$i]=${modlist[$i]}&"
done | awk '{print}' ORS=''
}
payload(){
echo -e "itemcount=${max}&$(concat)"
}
post(){
curl -s -X POST -H "Content-Type:application/x-www-form-urlencoded" -d "$(payload)" 'https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json'
}
result=$(post | jq -r '.[].publishedfiledetails[] | select(.result==1) | "\(.file_size) \(.publishedfileid)"')
echo "$result" > /tmp/modsizes
readarray -t newlist <<< $(echo -e "$result" | awk '{print $2}')
}
validate_mods(){
url="https://steamcommunity.com/sharedfiles/filedetails/?id="
newlist=()
readarray -t modlist <<< $remote_mods
query_defunct
}
server_modlist(){
for i in "${newlist[@]}"; do
printf "$i\n"
done
}
compare(){
diff=$(comm -23 <(server_modlist | sort -u) <(installed_mods | sort))
}
installed_mods(){
ls -1 "$workshop_dir"
}
concat_mods(){
if [[ -z ${remote_mods[@]} ]]; then
return 1
else
readarray -t serv <<< "$(server_modlist)"
for i in "${serv[@]}"; do
id=$(awk -F"= " '/publishedid/ {print $2}' "$workshop_dir"/$i/meta.cpp | awk -F\; '{print $1}')
encoded_id=$(encode $id)
link="@$encoded_id;"
echo -e "$link"
done | tr -d '\n' | perl -ple 'chop'
fi
}
launch(){
mods=$(concat_mods)
if [[ $debug -eq 1 ]]; then
launch_options="steam -applaunch $aid -connect=$ip -nolauncher -nosplash -name=$name -skipintro \"-mod=$mods\""
print_launch_options="$(printf "This is a dry run.\nThese options would have been used to launch the game:\n\n$launch_options\n" | fold -w 60)"
zenity --question --title="DZGUI" --ok-label="Write to file" --cancel-label="Back"\
--text="$print_launch_options" 2>/dev/null
if [[ $? -eq 0 ]]; then
source_script=$(realpath "$0")
source_dir=$(dirname "$source_script")
echo "$launch_options" > "$source_dir"/options.log
echo "[DZGUI] Wrote launch options to $source_dir/options.log"
zenity --info --width 500 --title="DZGUI" --text="Wrote launch options to \n$source_dir/options.log" 2>/dev/null
fi
else
echo "[DZGUI] All OK. Launching DayZ"
zenity --width 500 --title="DZGUI" --info --text="Launch conditions satisfied.\nDayZ will now launch after clicking [OK]." 2>/dev/null
steam -applaunch $aid -connect=$ip -nolauncher -nosplash -skipintro -name=$name \"-mod=$mods\"
exit
fi
one_shot_launch=0
}
browser(){
if [[ -n "$BROWSER" ]]; then
"$BROWSER" "$1" 2>/dev/null
else
xdg-open "$1" 2>/dev/null
fi
}
report_bug(){
echo "[DZGUI] Opening issues page in browser"
if [[ $is_steam_deck -eq 1 ]]; then
steam steam://openurl/"$issues_url" 2>/dev/null
elif [[ $is_steam_deck -eq 0 ]]; then
browser "$issues_url" 2>/dev/null &
fi
}
forum(){
echo "[DZGUI] Opening forum in browser"
if [[ $is_steam_deck -eq 1 ]]; then
steam steam://openurl/"$forum_url" 2>/dev/null
elif [[ $is_steam_deck -eq 0 ]]; then
browser "$forum_url" 2>/dev/null &
fi
}
help_file(){
echo "[DZGUI] Opening help file in browser"
if [[ $is_steam_deck -eq 1 ]]; then
steam steam://openurl/"$help_url" 2>/dev/null
elif [[ $is_steam_deck -eq 0 ]]; then
browser "$help_url" 2>/dev/null &
fi
}
set_mode(){
if [[ $debug -eq 1 ]]; then
mode=debug
else
mode=normal
fi
}
delete_by_id(){
new_whitelist="whitelist=\"$(echo "$whitelist" | sed "s/,$server_id$//;s/^$server_id,//;s/,$server_id,/,/;s/^$server_id$//")\""
mv $config_file ${config_path}dztuirc.old
nr=$(awk '/whitelist=/ {print NR}' ${config_path}dztuirc.old)
awk -v "var=$new_whitelist" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > ${config_path}dztuirc
echo "[DZGUI] Removed $server_id from key 'whitelist'"
zenity --info --title="DZGUI" --text="Removed "$server_id" from:\n${config_path}dztuirc\nIf errors occur, you can restore the file:\n${config_path}dztuirc.old" --width=500 2>/dev/null
source $config_file
}
delete_or_connect(){
if [[ $delete -eq 1 ]]; then
server_name=$(echo "$sel" | awk -F"%%" '{print $1}')
server_id=$(echo "$sel" | awk -F"%%" '{print $2}')
zenity --question --text="Delete this server? \n$server_name" --title=DZGUI --width=500 2>/dev/null
if [[ $? -eq 0 ]]; then
delete_by_id $server_id
fi
source $config_file
unset delete
else
#hotfix for bug #36
local lookup_ip=$(echo "$sel" | awk -F%% '{print $1}')
local lookup_port=$(echo "$lookup_ip" | awk -F: '{print $2}')
source $config_file
url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100\gameaddr\\$lookup_ip&key=$steam_api"
local qport_res=$(curl -Ls "$url" | jq -r --arg port $lookup_port '.response.servers[]|select(.gameport==($port|tonumber)).addr')
local qport=$(echo "$qport_res" | awk -F: '{print $2}')
qport_list="$lookup_ip%%$qport"
connect "$qport_list" "ip"
fi
}
populate(){
while true; do
if [[ $delete -eq 1 ]]; then
cols="--column="Server" --column="ID""
set_header "delete"
else
cols="--column="Server" --column="IP" --column="Players" --column="Gametime" --column="Status" --column="ID" --column="Ping""
set_header ${FUNCNAME[0]}
fi
rc=$?
if [[ $rc -eq 0 ]]; then
if [[ -z $sel ]]; then
warn "No item was selected."
else
delete_or_connect
return
fi
else
delete=0
return
fi
done
}
list_mods(){
if [[ -z $(installed_mods) ]] || [[ -z $(find $workshop_dir -maxdepth 2 -name "*.cpp" | grep .cpp) ]]; then
zenity --info --text="94: No mods currently installed or incorrect path given" $sd_res 2>/dev/null
else
for d in $(find $game_dir/* -maxdepth 1 -type l); do
dir=$(basename $d)
awk -v d=$dir -F\" '/name/ {printf "%s\t%s\n", $2,d}' "$gamedir"/$d/meta.cpp
done | sort
fi
}
fetch_query_ports(){
qport_list=$(echo "$response" | jq -r '.data[] .attributes | "\(.ip):\(.port)%%\(.portQuery)"')
}
connect_to_fav(){
if [[ -n $fav ]]; then
one_shot_launch=1
query_api
fetch_query_ports
echo "[DZGUI] Attempting connection to $fav_label"
connect "$qport_list" "ip"
one_shot_launch=0
else
warn "93: No fav server configured"
fi
}
set_header(){
[[ $auto_install -eq 2 ]] && install_mode="auto"
[[ $auto_install -eq 1 ]] && install_mode="headless"
[[ $auto_install -eq 0 ]] && install_mode=manual
if [[ $1 == "delete" ]]; then
sel=$(cat $tmp | zenity $sd_res --list $cols --title="DZGUI" --text="DZGUI $version | Mode: $mode | Branch: $branch | Mods: $install_mode | Fav: $fav_label" \
--separator="$separator" --print-column=1,2 --ok-label="Delete" 2>/dev/null)
elif [[ $1 == "populate" ]]; then
sel=$(cat $tmp | zenity $sd_res --list $cols --title="DZGUI" --text="DZGUI $version | Mode: $mode | Branch: $branch | Mods: $install_mode | Fav: $fav_label" \
--separator="$separator" --print-column=2,6 2>/dev/null)
elif [[ $1 == "main_menu" ]]; then
sel=$(zenity $sd_res --list --title="DZGUI" --text="${news}DZGUI $version | Mode: $mode | Branch: $branch | Mods: $install_mode | Fav: $fav_label" \
--cancel-label="Exit" --ok-label="Select" --column="Select launch option" --hide-header "${items[@]}" 2>/dev/null)
fi
}
toggle_branch(){
mv $config_file ${config_path}dztuirc.old
nr=$(awk '/branch=/ {print NR}' ${config_path}dztuirc.old)
if [[ $branch == "stable" ]]; then
branch="testing"
else
branch="stable"
fi
flip_branch="branch=\"$branch\""
awk -v "var=$flip_branch" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > $config_file
printf "[DZGUI] Toggled branch to '$branch'\n"
source $config_file
}
generate_log(){
cat <<-DOC
Linux: $(uname -mrs)
Version: $version
Branch: $branch
Whitelist: $whitelist
Steam path: $steam_path
Workshop path: $workshop_dir
Game path: $game_dir
Mods:
$(list_mods)
DOC
}
automods_prompt(){
cat <<- HERE
Headless installation set to ON.
READ THIS FIRST:
With this setting on, DZGUI will attempt to download and prepare mods using Valve's steamcmd tool.
The first time this process is run, DZGUI will ask you to select a terminal emulator of your preference to spawn the installation routine. If you don't have a preference or don't know, you can pick any.
Installation will kick off in a separate window and may ask you for input such as your sudo password in order to install system packages and create the steamcmd user.
steamcmd itself will ask for your Steam credentials. This information is used directly by Valve's steamcmd tool to authenticate your account and let you download mods headlessly. steamcmd is an official program created by Valve and communicates only with their servers.
NOTE: it can take some time for large mods to download, and steamcmd will not inform you of activity until each one is finished downloading.
If your distribution is unsupported, you don't have enough disk space to stage all of the mods, or there are other problems, DZGUI will warn you and write a report to $HOME/.local/share/dzgui/helpers/SCMD.log. You can attach this file to a bug report.
HERE
}
toggle_headless(){
[[ $is_steam_deck -eq 1 ]] && test_display_mode
[[ $gamemode -eq 1 ]] && { popup 200; return; }
mv $config_file ${config_path}dztuirc.old
local nr=$(awk '/auto_install=/ {print NR}' ${config_path}dztuirc.old)
if [[ $auto_install == "1" ]]; then
auto_install="0"
else
auto_install="1"
fi
local flip_state="auto_install=\"$auto_install\""
awk -v "var=$flip_state" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > $config_file
printf "[DZGUI] Set mod install to '$auto_install'\n"
source $config_file
local big_prompt
[[ $is_steam_deck -eq 1 ]] && big_prompt="--width=800"
[[ $auto_install == "1" ]] && zenity --info --text="$(automods_prompt)" $big_prompt 2>/dev/null
}
console_dl(){
readarray -t modids <<< "$@"
steam steam://open/console 2>/dev/null 1>&2 &&
sleep 1s
#https://github.com/jordansissel/xdotool/issues/67
local wid=$(xdotool search --desktop "$(xdotool get_desktop)" --name Steam)
xdotool windowactivate $wid
for i in "${modids[@]}"; do
xdotool type --delay 15 "workshop_download_item $aid $i"
sleep 0.2s
xdotool key Return
sleep 0.2s
done
}
find_default_path(){
discover(){
echo "# Searching for Steam"
default_steam_path=$(find / -type d \( -path "/proc" -o -path "*/timeshift" -o -path \
"/tmp" -o -path "/usr" -o -path "/boot" -o -path "/run" -o -path "/proc" -o -path "/root" \
-o -path "/sys" -o -path "/etc" -o -path "/var" -o -path "/lost+found" \) -prune \
-o -regex ".*/Steam/ubuntu12_32$" -print -quit 2>/dev/null | sed 's@/ubuntu12_32@@')
echo "100"
}
if [[ $is_steam_deck -eq 1 ]]; then
default_steam_path="$HOME/.local/share/Steam"
else
#default
if [[ -d "$HOME/.local/share/Steam" ]]; then
default_steam_path="$HOME/.local/share/Steam"
#ubuntu
elif [[ -d "$HOME/.steam/steam" ]]; then
default_steam_path="$HOME/.steam/steam"
else
local res=$(echo -e "Let DZGUI auto-discover Steam path (accurate, slower)\nSelect the Steam path manually (less accurate, faster)" | zenity --list --column="Choice" --title=DZGUI --hide-header --text="Steam is not installed in a standard location." $sd_res)
case "$res" in
*auto*) discover > >(zenity --width 500 --progress --auto-close --title="DZGUI" --pulsate --no-cancel 2>/dev/null) ;;
*manual*) file_picker ;;
esac
fi
fi
}
popup(){
pop(){
zenity --info --text="$1" --title=DZGUI --width=500 2>/dev/null
}
case "$1" in
100) pop "This feature requires xdotool.";;
200) pop "This feature is not supported on Gaming Mode.";;
300) pop "\nThe Steam console will now open and briefly issue commands to\ndownload the workshop files, then return to the download progress page.\n\nEnsure that the Steam console has keyboard and mouse focus\n(keep hands off keyboard) while the commands are being issued.\n\nDepending on the number if mods, it may take some time to queue the downloads,\nbut if a popup or notification window steals focus, it could obstruct\nthe process." ;;
400) pop "Automod install enabled. Auto-downloaded mods will not appear\nin your Steam Workshop subscriptions, but DZGUI will\ntrack the version number of downloaded mods internally\nand trigger an update if necessary." ;;
500) pop "Automod install disabled.\nSwitched to manual mode." ;;
600) pop "No preferred servers set." ;;
esac
}
requires_xdo(){
zenity --info --text="This feature requires xdotool" --title=DZGUI --width=500
}
toggle_console_dl(){
[[ $is_steam_deck -eq 1 ]] && test_display_mode
[[ $gamemode -eq 1 ]] && { popup 200; return; }
[[ ! $(command -v xdotool) ]] && { popup 100; return; }
mv $config_file ${config_path}dztuirc.old
local nr=$(awk '/auto_install=/ {print NR}' ${config_path}dztuirc.old)
if [[ $auto_install == "2" ]]; then
auto_install="0"
popup 500
else
auto_install="2"
popup 400
fi
local flip_state="auto_install=\"$auto_install\""
awk -v "var=$flip_state" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > $config_file
printf "[DZGUI] Set mod install to '$auto_install'\n"
source $config_file
}
force_update_mods(){
if [[ -f $version_file ]]; then
awk '{OFS="\t"}{$2="000"}1' $version_file > /tmp/versions
mv /tmp/versions $version_file
fi
}
options_menu(){
case "$auto_install" in
0) auto_hr="OFF"; headless_hr="OFF" ;;
1) auto_hr="OFF"; headless_hr="ON" ;;
2) auto_hr="ON"; headless_hr="OFF" ;;
esac
debug_list=(
"Toggle branch"
"Toggle debug mode"
"Generate debug log"
"Toggle auto mod install [$auto_hr]"
# "Toggle headless mod install [$headless_hr]"
# "Set auto-mod staging directory [$staging_dir]"
)
[[ $auto_install -eq 2 ]] && debug_list+=("Force update local mods")
debug_sel=$(zenity --list --width=1280 --height=800 --column="Options" --title="DZGUI" --hide-header "${debug_list[@]}" 2>/dev/null)
if [[ $debug_sel == "${debug_list[0]}" ]]; then
enforce_dl=1
toggle_branch &&
check_version
elif [[ $debug_sel == "${debug_list[1]}" ]]; then
toggle_debug
elif [[ $debug_sel == "${debug_list[2]}" ]]; then
source_script=$(realpath "$0")
source_dir=$(dirname "$source_script")
generate_log > "$source_dir/DZGUI.log"
printf "[DZGUI] Wrote log file to %s/log\n" "$source_dir"
zenity --info --width 500 --title="DZGUI" --text="Wrote log file to \n$source_dir/log" 2>/dev/null
elif [[ $debug_sel == "${debug_list[3]}" ]]; then
toggle_console_dl
elif [[ $debug_sel == "${debug_list[4]}" ]]; then
force_update=1
force_update_mods
merge_modlists > >(zenity --pulsate --progress --auto-close --title=DZGUI --width=500 2>/dev/null)
[[ -z $(is_steam_running) ]] && { zenity --info --text "Steam must be running on the current desktop to use this feature."; return; }
auto_mod_install
fi
}
query_and_connect(){
[[ -z $whitelist ]] && { popup 600; return; }
query_api
parse_json
#TODO: create logger function
if [[ ! $delete -eq 1 ]]; then
echo "[DZGUI] Checking response time of servers"
create_array | zenity --width 500 --progress --pulsate --title="DZGUI" --auto-close 2>/dev/null
else
create_array
fi
rc=$?
if [[ $rc -eq 1 ]]; then
:
else
populate
fi
}
exclude_full(){
response=$(echo "$response" | jq '[.[]|select(.players!=.max_players)]')
}
exclude_empty(){
response=$(echo "$response" | jq '[.[]|select(.players!=0)]')
}
filter_maps(){
echo "# Filtering maps"
[[ $ret -eq 98 ]] && return
local maps=$(echo "$response" | jq -r '.[].map//empty|ascii_downcase' | sort -u)
local map_ct=$(echo "$maps" | wc -l)
local map_sel=$(echo "$maps" | zenity --list --column="Check" --width=1200 --height=800 2>/dev/null --title="DZGUI" --text="Found $map_ct map types")
echo "[DZGUI] Selected '$map_sel'"
if [[ -z $map_sel ]]; then
ret=97
return
fi
echo "100"
response=$(echo "$response" | jq --arg map "$map_sel" '[.[]|select(.map)//empty|select(.map|ascii_downcase == $map)]')
}
exclude_daytime(){
response=$(echo "$response" | jq '[.[]|select(.gametype|test(",[0][6-9]:|,[1][0-6]:")|not)]')
}
exclude_nighttime(){
response=$(echo "$response" | jq '[.[]|select(.gametype|test(",[1][7-9]:|,[2][0-4]:|[0][0-5]:")|not)]')
}
keyword_filter(){
response=$(echo "$response" | jq --arg search "$search" '[.[]|select(.name|ascii_downcase | contains($search))]')
}
exclude_lowpop(){
response=$(echo "$response" | jq '[.[]|select(.players > 9)]')
}
exclude_nonascii(){
response=$(echo "$response" | jq -r '[.[]|select(.name|test("^([[:ascii:]])*$"))]')
}
strip_null(){
response=$(echo "$response" | jq -r '[.[]|select(.map//empty)]')
}
local_latlon(){
if [[ -z $(command -v dig) ]]; then
local local_ip=$(curl -Ls "https://ipecho.net/plain")
else
local local_ip=$(dig +short myip.opendns.com @resolver1.opendns.com)
fi
local url="http://ip-api.com/json/$local_ip"
local res=$(curl -Ls "$url" | jq -r '"\(.lat),\(.lon)"')
local_lat=$(echo "$res" | awk -F, '{print $1}')
local_lon=$(echo "$res" | awk -F, '{print $2}')
}
disabled(){
if [[ -z ${disabled[@]} ]]; then
printf "%s" "-"
else
for((i=0;i<${#disabled[@]};i++)); do
if [[ $i < $((${#disabled[@]}-1)) ]]; then
printf "%s, " "${disabled[$i]}"
else
printf "%s" "${disabled[$i]}"
fi
done
fi
}
pagination(){
if [[ ${#qport[@]} -eq 1 ]]; then
entry=server
else
entry=servers
fi
printf "DZGUI %s | " "$version"
printf "Mode: %s |" "$mode"
printf "Fav: %s " "$fav_label"
printf "\nIncluded: %s | " "$filters"
printf "Excluded: %s " "$(disabled)"
if [[ -n $search ]]; then
printf "| Keyword: %s " "$search"
fi
printf "\nReturned: %s %s of %s | " "${#qport[@]}" "$entry" "$total_servers"
printf "Players online: %s" "$players_online"
}
check_geo_file(){
local gzip="$helpers_path/ips.csv.gz"
curl -Ls "$sums_url" > "$sums_path"
cd "$helpers_path"
md5sum -c "$sums_path" 2>/dev/null 1>&2
local res=$?
cd $OLDPWD
if [[ $res -eq 1 ]]; then
run(){
mkdir -p "$helpers_path"
echo "# Fetching new geolocation DB"
curl -Ls "$db_file" > "$gzip"
echo "# Extracting coordinates"
#force overwrite
gunzip -f "$gzip"
echo "# Preparing helper file"
curl -Ls "$km_helper_url" > "$km_helper"
chmod +x $km_helper
echo "100"
}
run > >(zenity --pulsate --progress --auto-close --width=500 2>/dev/null)
fi
}
choose_filters(){
if [[ $is_steam_deck -eq 0 ]]; then
sd_res="--width=1920 --height=1080"
fi
sels=$(zenity --title="DZGUI" --text="Server search" --list --checklist --column "Check" --column "Option" --hide-header TRUE "All maps (untick to select from map list)" TRUE "Daytime" TRUE "Nighttime" False "Empty" False "Full" False "Low population" FALSE "Non-ASCII titles" FALSE "Keyword search" $sd_res 2>/dev/null)
if [[ $sels =~ Keyword ]]; then
search=$(zenity --entry --text="Search (case insensitive)" --width=500 --title="DZGUI" 2>/dev/null | awk '{print tolower($0)}')
[[ -z $search ]] && { ret=97; return; }
fi
[[ -z $sels ]] && return
filters=$(echo "$sels" | sed 's/|/, /g;s/ (untick to select from map list)//')
echo "[DZGUI] Filters: $filters"
}
get_dist(){
local given_ip="$1"
local network="$(echo "$given_ip" | awk -F. '{OFS="."}{print $1"."$2}')"
local binary=$(grep -E "^$network\." $geo_file)
local three=$(echo $given_ip | awk -F. '{print $3}')
local host=$(echo $given_ip | awk -F. '{print $4}')
local res=$(echo "$binary" | awk -F[.,] -v three=$three -v host=$host '$3 <=three && $7 >= three{if($3>three || ($3==three && $4 > host) || $7 < three || ($7==three && $8 < host)){next}{print}}' | awk -F, '{print $7,$8}')
local remote_lat=$(echo "$res" | awk '{print $1}')
local remote_lon=$(echo "$res" | awk '{print $2}')
if [[ -z $remote_lat ]]; then
local dist="Unknown"
echo "$dist"
else
local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
printf "%05.0f %s" "$dist" "km"
fi
}
prepare_filters(){
echo "# Filtering list"
[[ ! "$sels" =~ "Full" ]] && { exclude_full; disabled+=("Full") ; }
[[ ! "$sels" =~ "Empty" ]] && { exclude_empty; disabled+=("Empty") ; }
[[ ! "$sels" =~ "Daytime" ]] && { exclude_daytime; disabled+=("Daytime") ; }
[[ ! "$sels" =~ "Nighttime" ]] && { exclude_nighttime; disabled+=("Nighttime") ; }
[[ ! "$sels" =~ "Low population" ]] && { exclude_lowpop; disabled+=("Low-pop") ; }
[[ ! "$sels" =~ "Non-ASCII titles" ]] && { exclude_nonascii; disabled+=("Non-ASCII") ; }
[[ -n "$search" ]] && keyword_filter
strip_null
echo "100"
}
write_fifo(){
[[ -p $fifo ]] && rm $fifo
mkfifo $fifo
for((i=0;i<${#qport[@]};i++)); do
printf "%s\n%s\n%s\n%03d\n%03d\n%s\n%s:%s\n%s\n" "${map[$i]}" "${name[$i]}" "${gametime[$i]}" \
"${players[$i]}" "${max[$i]}" "$(get_dist ${addr[$i]})" "${addr[$i]}" "${gameport[$i]}" "${qport[$i]}" >> $fifo
done
}
munge_servers(){
if [[ ! "$sels" =~ "All maps" ]]; then
filter_maps > >(zenity --pulsate --progress --auto-close --width=500 2>/dev/null)
disabled+=("All maps")
fi
[[ $ret -eq 97 ]] && return
prepare_filters > >(zenity --pulsate --progress --auto-close --width=500 2>/dev/null)
if [[ $(echo "$response" | jq 'length') -eq 0 ]]; then
zenity --error --text="No matching servers" 2>/dev/null
return
fi
local addr=$(echo "$response" | jq -r '.[].addr' | awk -F: '{print $1}')
local gameport=$(echo "$response" | jq -r '.[].gameport')
local qport=$(echo "$response" | jq -r '.[].addr' | awk -F: '{print $2}')
#jq bug #1788, raw output cannot be used with ASCII
local name=$(echo "$response" | jq -a '.[].name' | sed 's/\\u[0-9a-z]\{4\}//g;s/^"//;s/"$//')
local players=$(echo "$response" | jq -r '.[].players')
local max=$(echo "$response" | jq -r '.[].max_players')
local map=$(echo "$response" | jq -r '.[].map|ascii_downcase')
local gametime=$(echo "$response" | jq -r '.[].gametype' | grep -oE '[0-9]{2}:[0-9]{2}$')
readarray -t qport <<< $qport
readarray -t gameport <<< $gameport
readarray -t addr <<< $addr
readarray -t name <<< $name
readarray -t players <<< $players
readarray -t map <<< $map
readarray -t max <<< $max
readarray -t gametime <<< $gametime
if [[ $is_steam_deck -eq 0 ]]; then
sd_res="--width=1920 --height=1080"
fi
write_fifo &
pid=$!
local sel=$(zenity --text="$(pagination)" --title="DZGUI" --list --column=Map --column=Name --column=Gametime --column=Players --column=Max --column=Distance --column=IP --column=Qport $sd_res --print-column=7,8 --separator=%% 2>/dev/null < <(while true; do cat $fifo; done))
if [[ -z $sel ]]; then
rm $fifo
kill -9 $pid
else
rm $fifo
kill -9 $pid
echo $sel
fi
}
debug_servers(){
[[ -f $debug_log ]] && rm $debug_log
if [[ -n $steam_api ]]; then
exists=true
else
exists=false
fi
key_len=${#steam_api}
first_char=${steam_api:0:1}
last_char=${steam_api:0-1}
debug_res=$(curl -Ls "https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100&limit=10&key=$steam_api")
debug_len=$(echo "$debug_res" | jq '[.response.servers[]]|length')
[[ -z $debug_len ]] && debug_len=0
cat <<-DOC > $debug_log
======START DEBUG======
Key exists: $exists
First char: $first_char
Last char: $last_char
Key length: $key_len
======Short query======
Expected: 10
Found: $debug_len
Response follows---->
$debug_res
======END DEBUG=======
DOC
}
server_browser(){
check_steam_api
[[ $? -eq 1 ]] && return
unset ret
file=$(mktemp)
local limit=20000
local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100&limit=$limit&key=$steam_api"
check_geo_file
local_latlon
choose_filters
[[ -z $sels ]] && return
[[ $ret -eq 97 ]] && return
#TODO: some error handling here
fetch(){
echo "# Getting server list"
response=$(curl -Ls "$url" | jq -r '.response.servers')
}
fetch > >(zenity --pulsate --progress --auto-close --width=500 2>/dev/null)
total_servers=$(echo "$response" | jq 'length')
players_online=$(echo "$response" | jq '.[].players' | awk '{s+=$1}END{print s}')
debug_log="$HOME/.local/share/dzgui/DEBUG.log"
debug_servers
local sel=$(munge_servers)
if [[ -z $sel ]]; then
unset filters
unset search
ret=98
return
fi
local sel_ip=$(echo "$sel" | awk -F%% '{print $1}')
local sel_port=$(echo "$sel" | awk -F%% '{print $2}')
qport_list="$sel_ip%%$sel_port"
if [[ -n "$sel_ip" ]]; then
connect "$sel_ip" "ip"
else
return
fi
}
mods_disk_size(){
printf "Total size on disk: %s | " $(du -sh "$game_dir" | awk '{print $1}')
printf "%s mods | " $(ls -1 "$game_dir" | wc -l)
printf "Location: %s/steamapps/workshop/content/221100" "$steam_path"
}
main_menu(){
print_news
set_mode
# if [[ -n $fav ]]; then
# items[8]=" Change favorite server"
# fi
while true; do
set_header ${FUNCNAME[0]}
rc=$?
if [[ $rc -eq 0 ]]; then
if [[ -z $sel ]]; then
warn "No item was selected."
elif [[ $sel == "${items[0]}" ]]; then
:
elif [[ $sel == "${items[1]}" ]]; then
server_browser
elif [[ $sel == "${items[2]}" ]]; then
query_and_connect
elif [[ $sel == "${items[3]}" ]]; then
connect_to_fav
elif [[ $sel == "${items[4]}" ]]; then
connect_by_ip
elif [[ $sel == "${items[5]}" ]]; then
history_table
elif [[ $sel == "${items[6]}" ]]; then
:
elif [[ $sel == "${items[7]}" ]]; then
add_by_id
elif [[ $sel == "${items[8]}" ]]; then
add_by_fav
elif [[ $sel == "${items[9]}" ]]; then
delete=1
query_and_connect
elif [[ $sel == "${items[10]}" ]]; then
:
elif [[ $sel == "${items[11]}" ]]; then
list_mods | sed 's/\t/\n/g' | zenity --list --column="Mod" --column="Symlink" \
--title="DZGUI" $sd_res --text="$(mods_disk_size)" \
--print-column="" 2>/dev/null
elif [[ $sel == "${items[12]}" ]]; then
changelog | zenity --text-info $sd_res --title="DZGUI" 2>/dev/null
elif [[ $sel == "${items[13]}" ]]; then
options_menu
main_menu
return
elif [[ $sel == "${items[14]}" ]]; then
:
elif [[ $sel == "${items[15]}" ]]; then
help_file
elif [[ $sel == "${items[16]}" ]]; then
report_bug
elif [[ $sel == "${items[17]}" ]]; then
forum
else
warn "This feature is not yet implemented."
fi
else
return
fi
done
}
page_through(){
list_response=$(curl -s "$page")
list=$(echo "$list_response" | jq -r '.data[] .attributes | "\(.name)\t\(.ip):\(.port)\t\(.players)/\(.maxPlayers)\t\(.details.time)\t\(.status)\t\(.id)"')
idarr+=("$list")
parse_json
}
parse_json(){
page=$(echo "$list_response" | jq -r '.links.next?')
if [[ $first_entry -eq 1 ]]; then
local list=$(echo "$list_response" | jq -r '.data[] .attributes | "\(.name)\t\(.ip):\(.port)\t\(.players)/\(.maxPlayers)\t\(.details.time)\t\(.status)\t\(.id)"')
idarr+=("$list")
first_entry=0
fi
if [[ "$page" != "null" ]]; then
local list=$(echo "$list_response" | jq -r '.data[] .attributes | "\(.name)\t\(.ip):\(.port)\t\(.players)/\(.maxPlayers)\t\(.details.time)\t\(.status)\t\(.id)"')
idarr+=("$list")
page_through
else
printf "%s\n" "${idarr[@]}" > $tmp
idarr=()
fetch_query_ports
fi
}
check_ping(){
ping_ip=$(echo "$1" | awk -F'\t' '{print $2}' | awk -F: '{print $1}')
ms=$(ping -c 1 -W 1 "$ping_ip" | awk -Ftime= '/time=/ {print $2}')
if [[ -z $ms ]]; then
echo "Timeout"
else
echo "$ms"
fi
}
create_array(){
rows=()
list=$(cat $tmp)
#TODO: improve error handling for null values
lc=1
while read line; do
name=$(echo "$line" | awk -F'\t' '{print $1}')
#truncate names
if [[ $(echo "$name" | wc -m) -gt 50 ]]; then
name="$(echo "$name" | awk '{print substr($0,1,50) "..."}')"
else
:
fi
ip=$(echo "$line" | awk -F'\t' '{print $2}')
players=$(echo "$line" | awk -F'\t' '{print $3}')
time=$(echo "$line" | awk -F'\t' '{print $4}')
stat=$(echo "$line" | awk -F'\t' '{print $5}')
#TODO: probe offline return codes
id=$(echo "$line" | awk -F'\t' '{print $6}')
tc=$(awk 'END{print NR}' $tmp)
if [[ $delete -eq 1 ]]; then
declare -g -a rows=("${rows[@]}" "$name" "$id")
else
echo "# Checking ping: $lc/$tc"
ping=$(check_ping "$line")
declare -g -a rows=("${rows[@]}" "$name" "$ip" "$players" "$time" "$stat" "$id" "$ping")
fi
let lc++
done <<< "$list"
for i in "${rows[@]}"; do echo -e "$i"; done > $tmp
}
set_fav(){
echo "[DZGUI] Querying favorite server"
query_api
fav_label=$(curl -s "$api" -H "Authorization: Bearer "$api_key"" -G -d "filter[game]=$game" -d "filter[ids][whitelist]=$fav" \
| jq -r '.data[] .attributes .name')
if [[ -z $fav_label ]]; then
fav_label=null
fi
echo "[DZGUI] Setting favorite server to '$fav_label'"
}
check_unmerged(){
if [[ -f ${config_path}.unmerged ]]; then
printf "[DZGUI] Found new config format, merging changes\n"
merge_config
rm ${config_path}.unmerged
fi
}
merge_config(){
source $config_file
mv $config_file ${config_path}dztuirc.old
[[ -z $staging_dir ]] && staging_dir="/tmp"
write_config > $config_file
printf "[DZGUI] Wrote new config file to %sdztuirc\n" $config_path
zenity --info --width 500 --title="DZGUI" --text="Wrote new config format to \n${config_path}dztuirc\nIf errors occur, you can restore the file:\n${config_path}dztuirc.old" 2>/dev/null
}
download_new_version(){
if [[ $is_steam_deck -eq 1 ]]; then
freedesktop_dirs
fi
source_script=$(realpath "$0")
source_dir=$(dirname "$source_script")
mv $source_script $source_script.old
echo "# Downloading version $upstream"
curl -Ls "$version_url" > $source_script
rc=$?
if [[ $rc -eq 0 ]]; then
echo "[DZGUI] Wrote $upstream to $source_script"
chmod +x $source_script
touch ${config_path}.unmerged
echo "100"
zenity --question --width 500 --title="DZGUI" --text "DZGUI $upstream successfully downloaded.\nTo view the changelog, select Changelog.\nTo use the new version, select Exit and restart." --ok-label="Changelog" --cancel-label="Exit" 2>/dev/null
code=$?
if [[ $code -eq 0 ]]; then
changelog | zenity --text-info $sd_res --title="DZGUI" 2>/dev/null
exit
elif [[ $code -eq 1 ]]; then
exit
fi
else
echo "100"
mv $source_script.old $source_script
zenity --info --title="DZGUI" --text "[ERROR] 99: Failed to download new version." 2>/dev/null
return
fi
}
check_branch(){
if [[ $branch == "stable" ]]; then
version_url="$stable_url/dzgui.sh"
elif [[ $branch == "testing" ]]; then
version_url="$testing_url/dzgui.sh"
fi
upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
}
enforce_dl(){
download_new_version > >(zenity --progress --pulsate --auto-close --no-cancel --width=500)
}
prompt_dl(){
zenity --question --title="DZGUI" --text "Version conflict.\n\nYour branch:\t\t\t$branch\nYour version\t\t\t$version\nUpstream version:\t\t$upstream\n\nVersion updates introduce important bug fixes and are encouraged.\n\nAttempt to download latest version?" --width=500 --ok-label="Yes" --cancel-label="No" 2>/dev/null
rc=$?
if [[ $rc -eq 1 ]]; then
return
else
echo "100"
download_new_version > >(zenity --progress --pulsate --auto-close --no-cancel --width=500)
fi
}
check_version(){
[[ -f $config_file ]] && source $config_file
[[ -z $branch ]] && branch="stable"
check_branch
[[ ! -f "$freedesktop_path/dzgui.desktop" ]] && freedesktop_dirs
if [[ $version == $upstream ]]; then
check_unmerged
else
# echo "100"
echo "[DZGUI] Upstream ($upstream) != local ($version)"
if [[ $enforce_dl -eq 1 ]]; then
enforce_dl
else
prompt_dl
fi
fi
}
check_architecture(){
cpu=$(cat /proc/cpuinfo | grep "AMD Custom APU 0405")
if [[ -n "$cpu" ]]; then
is_steam_deck=1
echo "[DZGUI] Setting architecture to 'Steam Deck'"
else
is_steam_deck=0
echo "[DZGUI] Setting architecture to 'desktop'"
fi
}
add_by_id(){
#FIXME: prevent redundant creation of existent IDs (for neatness)
while true; do
id=$(zenity --entry --text="Enter server ID" --title="DZGUI" 2>/dev/null)
rc=$?
if [[ $rc -eq 1 ]]; then
return
else
if [[ ! $id =~ ^[0-9]+$ ]]; then
zenity --warning --title="DZGUI" --text="Invalid ID" 2>/dev/null
else
[[ -z $whitelist ]] && new_whitelist="whitelist=\"$id\""
[[ -n $whitelist ]] && new_whitelist="whitelist=\"$whitelist,$id\""
mv $config_file ${config_path}dztuirc.old
nr=$(awk '/whitelist=/ {print NR}' ${config_path}dztuirc.old)
awk -v "var=$new_whitelist" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > ${config_path}dztuirc
echo "[DZGUI] Added $id to key 'whitelist'"
zenity --info --title="DZGUI" --text="Added "$id" to:\n${config_path}dztuirc\nIf errors occur, you can restore the file:\n${config_path}dztuirc.old" --width=500 2>/dev/null
source $config_file
return
fi
fi
done
}
toggle_debug(){
mv $config_file ${config_path}dztuirc.old
nr=$(awk '/debug=/ {print NR}' ${config_path}dztuirc.old)
if [[ $debug -eq 1 ]]; then
debug=0
else
debug=1
fi
flip_debug="debug=\"$debug\""
awk -v "var=$flip_debug" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > $config_file
printf "[DZGUI] Toggled debug flag to '$debug'\n"
source $config_file
}
setup(){
if [[ -n $fav ]]; then
set_fav
items[8]=" Change favorite server"
fi
}
check_map_count(){
count=1048576
echo "[DZGUI] Checking system map count"
if [[ $(sysctl -q vm.max_map_count | awk -F"= " '{print $2}') -lt $count ]]; then
echo "100"
map_warning=$(zenity --question --width 500 --title="DZGUI" --text "System map count must be $count or higher to run DayZ with Wine. Increase map count and make this change permanent? (will prompt for sudo password)" 2>/dev/null)
if [[ $? -eq 0 ]]; then
pass=$(zenity --password)
sudo -S <<< "$pass" sh -c "echo 'vm.max_map_count=1048576' > /etc/sysctl.d/dayz.conf"
echo ""
fi
fi
}
add_by_fav(){
while true; do
fav_id=$(zenity --entry --text="Enter server ID" --title="DZGUI" 2>/dev/null)
rc=$?
if [[ $rc -eq 1 ]]; then
return
else
if [[ ! $fav_id =~ ^[0-9]+$ ]]; then
zenity --warning --title="DZGUI" --text="Invalid ID"
else
new_fav="fav=\"$fav_id\""
mv $config_file ${config_path}dztuirc.old
nr=$(awk '/fav=/ {print NR}' ${config_path}dztuirc.old)
awk -v "var=$new_fav" -v "nr=$nr" 'NR==nr {$0=var}{print}' ${config_path}dztuirc.old > ${config_path}dztuirc
echo "[DZGUI] Added $fav_id to key 'fav'"
zenity --info --title="DZGUI" --text="Added "$fav_id" to:\n${config_path}dztuirc\nIf errors occurred, you can restore the file:\n${config_path}dztuirc.old" 2>/dev/null
source $config_file
set_fav
items[8]=" Change favorite server"
return
fi
fi
done
}
lock(){
if [[ ! -f $config_path/.lockfile ]]; then
touch $config_path/.lockfile
fi
pid=$(cat $config_path/.lockfile)
ps -p $pid -o pid= >/dev/null 2>&1
res=$?
if [[ $res -eq 0 ]]; then
echo "[DZGUI] Already running ($pid)"
zenity --info --text="DZGUI already running (pid $pid)" --width=500 --title="DZGUI" 2>/dev/null
exit
elif [[ $pid == $$ ]]; then
:
else
echo $$ > $config_path/.lockfile
fi
}
fetch_helpers(){
mkdir -p "$helpers_path"
[[ ! -f "$helpers_path/vdf2json.py" ]] && curl -Ls "$vdf2json_url" > "$helpers_path/vdf2json.py"
}
initial_setup(){
echo "# Initial setup"
run_depcheck
watcher_deps
check_architecture
check_version
check_map_count
fetch_helpers
config
run_varcheck
stale_symlinks
init_items
setup
echo "100"
}
main(){
lock
initial_setup > >(zenity --pulsate --progress --auto-close --title="DZGUI" --no-cancel --width=500 2>/dev/null)
main_menu
#cruddy handling for steam forking
[[ $? -eq 1 ]] && pkill -f dzgui.sh
}
main