#!/usr/bin/env bash
set -o pipefail
version=5.6.0

#CONSTANTS
aid=221100
game="dayz"
app_name="dzgui"
app_name_upper="DZGUI"
workshop="steam://url/CommunityFilePage/"
sd_res="--width=1280 --height=800"
steamsafe_zenity="/usr/bin/zenity"
separator="␞"

##CONFIG
config_path="$HOME/.config/dztui"
config_file="$config_path/dztuirc"
source $config_file

#PATHS
state_path="$HOME/.local/state/$app_name"
cache_path="$HOME/.cache/$app_name"
share_path="$HOME/.local/share/$app_name"
script_path="$share_path/dzgui.sh"
helpers_path="$share_path/helpers"
prefix="dzg"

#LOGS
log_path="$state_path/logs"
debug_log="$log_path/DZGUI_DEBUG.log"
system_log="$log_path/DZGUI_SYSTEM.log"
mod_log="$log_path/DZGUI_MODIDS.log"
meta_log="$log_path/DZGUI_META.log"

#STATE FILES
history_file="$state_path/$prefix.history"
versions_file="$state_path/$prefix.versions"
lock_file="$state_path/$prefix.lock"

#CACHE
cache_dir="$HOME/.cache/$app_name"
_cache_servers="$cache_dir/$prefix.servers"
_cache_mods_temp="$cache_dir/$prefix.mods_temp"
_cache_stale_mods_temp="$cache_dir/$prefix.stale_mods_temp"
_cache_temp="$cache_dir/$prefix.temp"
_cache_my_servers="$cache_dir/$prefix.my_servers"
_cache_history="$cache_dir/$prefix.history"
_cache_launch="$cache_dir/$prefix.launch_mods"
_cache_address="$cache_dir/$prefix.launch_address"
_cache_coords="$cache_path/$prefix.coords"
_cache_cooldown="$cache_path/$prefix.cooldown"
_cache_lan="$cache_path/$prefix.lan"

#XDG
freedesktop_path="$HOME/.local/share/applications"

#HELPERS
ui_helper="$helpers_path/ui.py"
geo_file="$helpers_path/ips.csv"
km_helper="$helpers_path/latlon"
sums_path="$helpers_path/sums.md5"
query_helper="$helpers_path/query_v2.py"
func_helper="$helpers_path/funcs"
lan_helper="$helpers_path/lan"

#STEAM PATHS
workshop_path="$steam_path/steamapps/workshop"
workshop_dir="$workshop_path/content/$aid"
downloads_dir="$workshop_path/downloads/$aid"
game_dir="$steam_path/steamapps/common/DayZ"

#URLS
author="aclist"
repo="dztui"
gh_prefix="https://github.com"
issues_url="$gh_prefix/$author/$repo/issues"
url_prefix="https://raw.githubusercontent.com/$author/$repo"
stable_url="$url_prefix/dzgui"
testing_url="$url_prefix/testing"
releases_url="$gh_prefix/$author/$repo/releases/download/browser"
help_url="https://$author.github.io/dzgui/index"
help_url2="https://$author.codeberg.page"
forum_url="https://old.reddit.com/r/dzgui"
sponsor_url="$gh_prefix/sponsors/$author"
battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz"
steam_api_url="https://steamcommunity.com/dev/apikey"
#TODO: update link in docs
battlemetrics_api_url="https://www.battlemetrics.com/developers"
bm_api="https://api.battlemetrics.com/servers"

if [[ $preferred_client == "steam" ]]; then
    steam_cmd="steam"
else
    steam_cmd="flatpak run com.valvesoftware.Steam"
fi

declare -A funcs=(
["Highlight stale"]="find_stale_mods"
["My servers"]="dump_servers"
["Change player name"]="update_config_val"
["Change Steam API key"]="update_config_val"
["Change Battlemetrics API key"]="update_config_val"
["Change favorite server"]="add_record"
["Quick-connect to favorite server"]="quick_connect"
["Add server by IP"]="add_record"
["Add server by ID"]="add_record"
["Connect by IP"]="validate_and_connect"
["Connect by ID"]="validate_and_connect"
["Connect from table"]="connect_from_table"
["find_id"]="find_id"
["toggle"]="toggle"
["Open link"]="open_link"
["filter"]="dump_servers"
["dump_servers"]="dump_servers"
["get_unique_maps"]="get_unique_maps"
["get_dist"]="get_dist"
["test_cooldown"]="test_cooldown"
["query_config"]="query_config"
["start_cooldown"]="start_cooldown"
["List installed mods"]="list_mods"
["Delete selected mods"]="delete_local_mod"
["align_local"]="align_versions_file"
["show_server_modlist"]="show_server_modlist"
["test_ping"]="test_ping"
["is_in_favs"]="is_in_favs"
["show_log"]="show_log"
["Output system info to log file"]="generate_log"
["open_user_workshop"]="open_user_workshop"
["open_workshop_page"]="open_workshop_page"
["Add to my servers"]="update_favs_from_table"
["Remove from my servers"]="update_favs_from_table"
["Remove from history"]="remove_from_history"
["Force update local mods"]="force_update"
["Handshake"]="final_handshake"
["get_player_count"]="get_player_count"
["lan_scan"]="lan_scan"
["update_symlinks"]="update_symlinks"
)

lan_scan(){
    local port="$1"
    local res
    res=$("$lan_helper" "$port")
    if [[ -z $res ]]; then
        printf "\n"
    else
        printf "%s\n" "$res"
    fi
}
find_stale_mods(){
    local res
    local mods=()
    > $_cache_stale_mods_temp
    for i in "${ip_list[@]}"; do
        local ip=$(<<< "$i" awk -F: '{print $1}')
        local qport=$(<<< "$i" awk -F: '{print $3}')
        res=$(a2s $ip $qport rules)
        if [[ -n $res ]]; then
            printf "%s\n" "$res" >> $_cache_stale_mods_temp
        fi
    done
    printf ""
    return 99
}
get_player_count(){
    shift
    local res
    local ip="$1"
    local qport="$2"
    res=$(a2s $ip $qport info)
    [[ ! $? -eq 0 ]] && return 1
    local players="$(<<< $res jq -r '.[].players')"
    local queue="$(<<< $res jq -r '.[].gametype|split("lqs")[1]|split(",")[0]')"
    printf "%s\n%s" "$players" "$queue"
}

validate_and_connect(){
    local context="$1"
    local addr="$2"

    local record
    case "$context" in
        "Connect by ID")
            if [[ -z "$api_key" ]]; then
                printf "No Battlemetrics API key set"
                return 4
            fi
            record=$(map_id_to_ip "$addr")
            if [[ $? -eq 1 ]]; then
                logger WARN "Not a valid record: '$addr'"
                printf "Not a valid ID"
                return 2
            fi
            logger INFO "Battlemetrics ID resolved to IP $record"
            ;;
        "Connect by IP")
            if [[ $(validate_ip "$addr") -eq 1 ]]; then
                printf "Not a valid IP format. Supply IP:Queryport"
                return 2
            fi
            local ip=$(<<< $addr awk -F: '{print $1}')
            local qport=$(<<< $addr awk -F: '{print $2}')
            local res
            res=$(a2s $ip $qport info)
            if [[ ! $? -eq 0 ]]; then
                printf "Timed out when querying the server. Is this a valid server?"
                return 2
            fi
            local gameport="$(<<< $res jq -r '.[].gameport')"
            record="${ip}:${gameport}:${qport}"
            logger INFO "Record resolved to $record"
    esac
    try_connect "$record"
}
map_id_to_ip(){
    local id="$1"
    local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
        -G -d "sort=-players" \
        -d "filter[game]=$game" \
        -d "filter[ids][whitelist]=$id")
    local len=$(<<< "$res" jq '.data|length')
    [[ $len -eq 0 ]] && return 1
    local record=$(<<< "$res" jq -r '.data[].attributes|"\(.ip):\(.port):\(.portQuery)"')
    echo "$record"
}
add_record(){
    local context="$1"
    local addr="$2"
    local record
    if [[ $context != "Add server by ID" ]] && [[ $(validate_ip "$addr") -eq 1 ]]; then
        printf "Not a valid IP format. Supply IP:Queryport"
        return 2
    fi
    local ip=$(<<< $addr awk -F: '{print $1}')
    local qport=$(<<< $addr awk -F: '{print $2}')
    local res
    res=$(a2s $ip $qport info)
    if [[ ! $? -eq 0 ]]; then
        printf "Timed out when querying the server. Is this a valid server?"
        return 2
    fi
    local gameport="$(<<< $res jq -r '.[].gameport')"
    record="${ip}:${gameport}:${qport}"

    case "$context" in
        "Add server by IP")
            if [[ ${ip_list[*]} =~ $record ]]; then
                printf "Already in favorites list"
                return 1
            fi
            add_to_favs "$record"
            ;;
        "Change favorite server")
            fav_label=$(<<< "$res" jq -r '.[].name')
            fav_server="$record"
            update_config
            echo "Updated favorite server to '$fav_server' ($fav_label)"
            return 90
            ;;
        "Add server by ID")
            if [[ -z "$api_key" ]]; then
                printf "No Battlemetrics API key set"
                return 4
            fi
            record=$(map_id_to_ip "$addr")
            if [[ $? -eq 1 ]]; then
                logger WARN "Not a valid record: '$addr'"
                printf "Not a valid ID"
                return 2
            fi
            logger INFO "Battlemetrics ID resolved to IP $record"
            if [[ ${ip_list[*]} =~ $record ]]; then
                printf "Already in favorites list"
                return 1
            fi
            add_to_favs "$record"
            ;;
    esac
}
connect_by_id(){
    if [[ $(validate_ip "$addr") -eq 1 ]]; then
        printf "Not a valid IP format. Supply IP:Queryport"
        return 2
    fi
    local ip=$(<<< $addr awk -F: '{print $1}')
    local qport=$(<<< $addr awk -F: '{print $2}')
    local res
    res=$(a2s $ip $qport info)
    if [[ ! $? -eq 0 ]]; then
        printf "Timed out when querying the server. Is this a valid server?"
        return 2
    fi
    #res contains modlist
}
start_cooldown(){
    logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
    date +%s > $_cache_cooldown
}
initialize_remote_servers(){
    local file="$_cache_servers"
    [[ -f $file ]] && rm "$file"
    local res
    res=$(get_remote_servers)
    parse_server_json "$res" >> "$file"
}
is_dlc(){
    local dlc
    local ip="$1"
    local gport="$2"
    local res="$(curl -Ls "https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\gameaddr\\${ip}:${gport}\appid\221100&key=$steam_api")"
    dlc=$(<<< "$res" jq '.response.servers[].gametype|contains("isDLC")')
    printf "%s\n" "$dlc"
}
a2s(){
    local ip="$1"
    local qport="$2"
    local mode="$3"
    logger INFO "Querying '$ip:$qport' with mode '$mode'"
    local res
    res=$(python3 "$query_helper" "$ip" "$qport" "$mode")
    if [[ $? -eq 1 ]]; then
        res=$(try_fallback "$ip" "$qport" "$mode")
        if [[ $? -eq 1 ]]; then
            return 1
        fi
    fi
    printf "%s\n" "$res"
}
is_in_favs(){
    shift
    local record="$1"
    for (( i = 0; i < ${#ip_list[@]}; i++ )); do
        if [[ ${ip_list[$i]} == "$record" ]]; then
            logger INFO "'$record' is in favorites list"
            return 0
        fi
    done
    return 1
}
find_id(){
    local file="$default_steam_path/config/loginusers.vdf"
    [[ ! -f $file ]] && return 1
    local res=$(python3 "$helpers_path/vdf2json.py" \
    -i "$file" \
    | jq -r '.users
        |to_entries[]
        |select(.value.MostRecent=="1")
        |.key'
    )
    [[ -z $res ]] && return 1
    printf "%s" "$res"
    return 0
}
list_mods(){
    local symlink
    local sep
    local name
    local base_dir
    local size
    local mods
    if [[ -z $(installed_mods) ]] || [[ -z $(find $workshop_dir -maxdepth 2 -name "*.cpp" | grep .cpp) ]]; then
        printf "No mods currently installed or incorrect path set."
        logger WARN "Found no locally installed mods"
        return 1
    else
        mods=$(for dir in $(find $game_dir/* -maxdepth 1 -type l); do
            symlink=$(basename $dir)
            sep="␞"
            name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp")
            base_dir=$(basename $(readlink -f $game_dir/$symlink))
            size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}')
            size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))")
            LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size"
        done | sort -k1)
        # user may have manually pruned mods out-of-band
        # handle directory detritus but no actual mods
        if [[ -z $mods ]]; then
            printf "No mods currently installed or incorrect path set."
            return 1
        fi
        echo "$mods"
    fi
}
installed_mods(){
    find "$workshop_dir" -maxdepth 1 -mindepth 1 -printf "%f\n"
}
local_latlon(){
    local url="http://ip-api.com/json/$local_ip"
    local local_ip

    if [[ -z $(command -v dig) ]]; then
        local_ip=$(curl -Ls "https://ipecho.net/plain")
    else
        local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
    fi
    curl -Ls "$url" | jq -r '"\(.lat)\n\(.lon)"'
}
get_dist(){
    shift
    local given_ip="$1"
    readarray -t coords < "$_cache_coords"
    readarray -t n < <(<<< "$given_ip" awk 'BEGIN{RS="."}{$1=$1}1')

    local local_lat="${coords[0]}"
    local local_lon="${coords[1]}"
    local network="^${n[0]}.${n[1]}\."
    local three="${n[2]}"
    local host="${n[3]}"

    local binary=$(grep -E "$network" $geo_file)
    local res=$(<<< "$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=$(<<< "$res" awk '{print $1}')
    local remote_lon=$(<<< "$res" awk '{print $2}')
    if [[ -z $remote_lat ]]; then
        logger WARN "Failed to find geolocation candidate in IP database"
        local dist="Unknown"
        printf "Unknown"
    else
        logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'"
        local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
        LC_NUMERIC=C printf "%d" "$dist"
        logger INFO "Distance: $dist km"
    fi
}
get_remote_servers(){
    params=(
        "\\nor\1\map\chernarusplus\\nor\1\map\sakhal"
        "\map\chernarusplus\empty\1"
        "\map\chernarusplus\noplayers\1"
        "\map\\sakhal"
    )
    local limit=10000
    local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?"

    _fetch(){
        local param="$1"
        curl -LsG "$url" \
            -d filter="\appid\221100${param}" \
            -d limit=$limit \
            -d key=$steam_api \
            | jq -M -r '.response.servers'

    }

    for ((i=0; i <${#params[@]}; i++ )); do
        _fetch "${params[$i]}" > $_cache_temp.${i}
    done

    jq -n '[ [inputs]|add ].[]' $_cache_temp.* && rm $_cache_temp.*
}
get_unique_maps(){
    shift
    local context="$1"
    local filter_file
    case "$context" in
        "My saved servers")
            filter_file="$_cache_my_servers"
            ;;
        "Server browser")
            filter_file="$_cache_servers"
            ;;
        "Recent servers")
            filter_file="$_cache_history"
    esac
    logger INFO "Map filter context is: '$context', using cached file at '$filter_file'"
    < "$filter_file" awk -F$separator '{print $2}' | sort -u
}
query_config(){
    [[ -n $2 ]] && local key=$2
    keys=(
        "branch"
        "debug"
        "auto_install"
        "name"
        "fav_label"
        "preferred_client"
        "fullscreen"
    )
    if [[ -n $key ]]; then
        if [[ -n ${!key} ]]; then
            echo "${!key}"
            return 0
        else
            echo ""
            return 1
        fi
    fi
    for i in "${keys[@]}"; do
        echo "${!i}"
    done
}
filter_servers(){
    local filtered="$(< "$1")"
    shift
    readarray -t filters < <(printf  "%s\n" "$@")

    for ((i=0; i< ${#filters[@]}; ++i)); do
        if [[ ${filters[$i]} =~ Keyword ]]; then
            keyword=$(<<< ${filters[$i]} awk -F␞ '{print $2}')
        elif [[ ${filters[$i]} =~ Map ]]; then
            map=$(<<< ${filters[$i]} awk -F= '{print $2}')
        fi
    done

    filter_ascii(){
        if [[ ${filters[*]} =~ Non ]]; then
            echo -n "$filtered"
        else
            <<< "$filtered" sed 's/␞/@@DZGUI_PLACEHOLDER@@/g' | grep -v -P '[^[:ascii:]]' | sed 's/@@DZGUI_PLACEHOLDER@@/␞/g'
        fi
    }
    filter_time(){
        if [[ ${filters[*]} =~ Day ]] && [[ ${filters[*]} =~ Night ]]; then
            echo -n "$filtered"
        elif [[ ${filters[*]} =~ Day ]]; then
            <<< "$filtered" awk -F$separator '$4~/^([0][6-9]:|[1][0-6])/'
        elif [[ ${filters[*]} =~ Night ]]; then
            <<< "$filtered" awk -F$separator '$4~/^([1][7-9]:|[2][0-3]:|[0][0-5])/'
        else
            echo -n ""
        fi
    }
    filter_perspective(){
        if [[ ${filters[*]} =~ 1PP ]] && [[ ${filters[*]} =~ 3PP ]]; then
            echo -n "$filtered"
        elif [[ ${filters[*]} =~ 1PP ]]; then
            <<< "$filtered" awk '!/3PP/'
        elif [[ ${filters[*]} =~ 3PP ]]; then
            <<< "$filtered" awk '!/1PP/'
        else
            echo -n ""
        fi
    }
    filter_lowpop(){
        if [[ ${filters[*]} =~ Low ]]; then
            echo -n "$filtered"
        else
            <<< "$filtered" awk -F$separator '{if (($5 > 0) && ($5/$6)*100 >=30){print $0}}'
        fi
    }
    filter_full(){
        if [[ ${filters[*]} =~ Full ]]; then
            echo -n "$filtered"
        else
            <<< "$filtered" awk -F$separator '$5 != $6'
        fi
    }
    filter_empty(){
        if [[ ${filters[*]} =~ Empty ]]; then
            echo -n "$filtered"
        else
            <<< "$filtered" awk -F$separator '$5 != "0"'
        fi
    }
    filter_map(){
        if [[ $map == "All maps" ]]; then
            echo "$filtered"
        else
            <<< "$filtered" awk -v var="$map" -F$separator '$2 == var'
        fi
    }
    filter_keyword(){
        keyword=$(sanitize "$keyword")
        <<< "$filtered" awk -F$separator -v keyword="$keyword" 'tolower($0) ~ tolower(keyword)'
    }
    filter_duplicates(){
        if [[ ${filters[*]} =~ Duplicate ]]; then
            echo -n "$filtered"
        else
            <<< "$filtered" awk -F$separator '!seen[$1]++'
        fi
    }

    filtered=$(filter_perspective)
    filtered=$(filter_full)
    filtered=$(filter_empty)
    filtered=$(filter_time)
    filtered=$(filter_map)
    filtered=$(filter_lowpop)
    filtered=$(filter_ascii)
    filtered=$(filter_duplicates)
    filtered=$(filter_keyword)

    if [[ -z "$filtered" ]]; then
        logger WARN "Filter result is empty"
        echo -n ""
        return
    fi

    logger INFO "Returning sorted server list back to UI"
    printf "%s\n" "$filtered" | sort -k1
}
sanitize(){
    echo "$1" | sed \
    -e 's/\//\\\//g' \
    -e 's/\$/\\$/g' \
    -e 's/\[/\\[/g' \
    -e 's/\]/\\]/g' \
    -e 's/\#/\\#/g' \
    -e 's/\./\\./g' \
    -e 's/\^/\\^/g' \
    -e 's/\=/\\=/g' \
    -e 's/|/\\|/g' \
    -e 's/\+/\\+/g' \
    -e 's/(/\\(/g' \
    -e 's/)/\\)/g'
}
parse_server_json(){
    local response="$1"
    # some servers pad SOH in name
    <<< "$response" sed 's/\\u0001//g' | jq -r '
    .[]|"\(.name)␞" +
    "\(.map|if type == "string" then ascii_downcase else "null" end)␞" +
    "\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "no3rd") then "1PP" else "3PP" end) end)␞" +
    "\(if .gametype == null then "null" else (.gametype as $time|$time|test("[0-9]{2}:[0-9]{2}$") as $match|(if $match == true then ($time|scan("[0-9]{2}:[0-9]{2}$")) else "XXXX" end)) end)␞" +
    "\(.players)␞" +
    "\(.max_players)␞" +
    "\(if .gametype == null then "0" elif .gametype|split("lqs")[1] == null then "0" else .gametype|split("lqs")[1]|split(",")[0] end)␞" +
    "\(.addr|split(":")[0]):\(if .gameport == null then "XXXX" else .gameport end)␞" +
    "\(.addr|split(":")[1])"
    ' | sort -k1
}
align_versions_file(){
    shift
    local mod="$1"
    [[ ! -f $versions_file ]] && return
    < "$versions_file" awk -F, -v var="$mod" '$1 != var' > $versions_file.new &&
        mv $versions_file.new $versions_file
    logger INFO "Removed local signatures for the mod '$mod'"
}
pluralize(){
    local plural="$1"
    local count="$2"
    local mod
    local suffix
    local base
    local ct
    local s

    if [[ "${plural: -2}" == "es" ]]; then
        base="${plural::-2}"
        suffix="${plural: -2}"
        ct=$((count^2))
        [[ $ct -ne 3 ]] && s="$suffix"
    else
        base="${plural::-1}"
        suffix="${plural: -1}"
        ct=$((count^1))
        [[ $ct -gt 0 ]] && s="$suffix"
    fi

    printf "%s%s" "$base" "$s"
}
delete_local_mod(){
    shift
    readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp)
    readarray -t ids < <(awk '{print $2}' $_cache_mods_temp)
    rm "$_cache_mods_temp"
    for ((i=0; i<${#symlinks[@]}; i++)); do
        [[ ! -d $workshop_dir/${ids[$i]} ]] && return 1
        [[ ! -L $game_dir/${symlinks[$i]} ]] && return 1
        #SC2115
        rm -rf "${workshop_dir:?}/${ids[$i]}" && unlink "$game_dir/${symlinks[$i]}" || return 1
        align_versions_file "align" "${ids[$i]}"
    done
    printf "Successfully deleted %s %s." "${#symlinks[@]}" "$(pluralize "mods" ${#symlinks[@]})"
    return 95
}
test_cooldown(){
    [[ ! -f $_cache_cooldown ]] && return 0
    local old_time=$(< $_cache_cooldown)
    local cur_time=$(date +%s)
    local delta=$(($cur_time - $old_time))
    if [[ $delta -lt 60 ]]; then
        local remains=$((60 - $delta))
        local suffix=$(pluralize "seconds" $remains)
        printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
        exit 1
    fi
}
dump_servers(){
    local context="$1"
    local subcontext="$2"
    local ip
    local qport
    local res
    _iterate(){
        local file="$1"
        shift
        for server in "$@"; do
            ip=$(<<< $server awk -F: '{print $1}')
            qport=$(<<< $server awk -F: '{print $3}')
            res=$(a2s "$ip" "$qport" info)
            if [[ ! $? -eq 0 ]]; then
                continue
            fi
            parse_server_json "$res" >> "$file"
        done
    }
    case "$subcontext" in
        *Server[[:space:]]browser*)
            local file="$_cache_servers"
            if [[ ! $subcontext =~ Name ]]; then
                initialize_remote_servers
            fi
            ;;
        *My[[:space:]]saved[[:space:]]servers*)
            local file="$_cache_my_servers"
            if [[ ! $subcontext =~ Name ]]; then
                [[ -f $file ]] && rm $file
                _iterate "$file" "${ip_list[@]}"
            fi
            ;;
        *Recent[[:space:]]servers*)
            local file="$_cache_history"
            if [[ ! $subcontext =~ Name ]]; then
                [[ -f $file ]] && rm $file
                readarray -t iters < <(cat $history_file)
                _iterate "$file" "${iters[@]}"
            fi
            ;;
        *Scan[[:space:]]LAN[[:space:]]servers*)
            local port=$(<<< "$subcontext" awk -F: '{print $2}')
            local file="$_cache_lan"
            if [[ ! $subcontext =~ Name ]]; then
                [[ -f $file ]] && rm $file
                local lan=$(lan_scan $port)
                readarray -t iters <<< "$lan"
                _iterate "$file" "${iters[@]}"
            fi
            ;;
    esac
    shift
    logger INFO "Server context is '$subcontext', reading from file '$file'"
    filter_servers "$file" "$@"
}
logger(){
    local date="$(date "+%F %T,%3N")"
    local tag="$1"
    local string="$2"
    local self="${BASH_SOURCE[0]}"
    local caller="${FUNCNAME[1]}"
    local line="${BASH_LINENO[0]}"
    self="$(<<< "$self" sed 's@\(/[^/]*/\)\([^/]*\)\(.*\)@\1REDACTED\3@g')"
    printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" >> "$debug_log"
}
test_ping(){
    shift
    local ip="$1"
    local qport="$2"
    local res
    res=$(ping -c1 -4 -W0.5 $1 | grep time= | awk -F= '{print $4}')
    [[ ! $? -eq 0 ]] && res="Timed out"
    printf "%s" "$res"
}
show_server_modlist(){
    shift
    local ip="$1"
    local qport="$2"
    local res=$(a2s $ip $qport names)
    [[ -z $res ]] && return 1
    [[ $(<<< $res jq '.ids|length') -lt 1 ]] && return 1
    local names=$(<<< "$res" jq -r '.names[]')
    local ids=$(<<< "$res" jq -r '.ids[]')
    local icon
    local flag
    local label
    readarray -t names <<< "$names"
    readarray -t ids <<< "$ids"
    readarray -t mods < <(installed_mods)
    for ((i=0; i<${#ids[@]}; ++i)); do
        icon=
        flag="WARN"
        label="MISSING"
        for j in "${mods[@]}"; do
            if [[ $j == "${ids[$i]}" ]]; then
                icon=✓
                flag="INFO"
                label="installed"
                break
            fi
        done
        logger $flag "Mod '${names[$i]}' is $label"
        printf "%s␞%s␞%s\n" "${names[$i]}" "${ids[i]}" "$icon"
    done | sort -k1
}
print_ip_list(){
    [[ ${#ip_list[@]} -eq 0 ]] && return 1
    printf "\t\"%s\"\n" "${ip_list[@]}"
}
write_config(){
cat <<-END
#Path to DayZ installation
steam_path="$steam_path"

#Battlemetrics API key
api_key="$api_key"

#Favorited server IP:PORT array
ip_list=(
$(print_ip_list)
)

#Favorite server to fast-connect to (limit one)
fav_server="$fav_server"

#Favorite server label (human readable)
fav_label="$fav_label"

#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"

#Start in fullscreen
fullscreen="$fullscreen"

#Steam API key
steam_api="$steam_api"

#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"

#Preferred Steam launch command (for Flatpak support)
preferred_client="$preferred_client"

#DZGUI source path
src_path="$src_path"
END
}
format_version_url(){
    case "$branch" in
        "stable")
            version_url="$stable_url/dzgui.sh"
            ;;
        "testing")
            version_url="$testing_url/dzgui.sh"
            ;;
    esac
    echo "$version_url"
}
get_response_code(){
    local url="$1"
    curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
}
test_connection(){
    source "$config_file"
    declare -A hr
    local res1
    local res2
    local str="No connection could be established to the remote server"
    hr=(
        ["github.com"]="https://github.com/$author"
        ["codeberg.org"]="https://codeberg.org/$author"
    )
    res=$(get_response_code "${hr["github.com"]}")
    if [[ $res -ne 200 ]]; then
        logger WARN "Remote host '${hr["github.com"]}' unreachable', trying fallback"
        remote_host=cb
        logger INFO "Set remote host to '${hr["codeberg.org"]}'"
        res=$(get_response_code "${hr["codeberg.org"]}")
        if [[ $res -ne 200 ]]; then
            return 1
        fi
    fi
    if [[ $remote_host == "cb" ]]; then
        url_prefix="https://codeberg.org/$author/$repo/raw/branch"
        releases_url="https://codeberg.org/$author/$repo/releases/download/browser"
        stable_url="$url_prefix/dzgui"
        testing_url="$url_prefix/testing"
    fi
}
download_new_version(){
    test_connection
    rc=$?
    if [[ $rc -eq 1 ]]; then
        printf "Remote resource unavailable. Aborting"
        logger WARN "Remote resource unavailable"
        return 1
    fi
    local version_url="$(format_version_url)"
    mv "$src_path" "$src_path.old"
    curl -Ls "$version_url" > "$src_path"
    rc=$?
    if [[ $rc -eq 0 ]]; then
        dl_changelog
        logger INFO "Wrote new version to $src_path"
        chmod +x "$src_path"
        touch "${config_path}.unmerged"
        printf "New version downloaded. Please exit to apply changes"
        logger INFO "User exited after version upgrade"
        return 255
    else
        mv "$src_path.old" "$src_path"
        printf "Failed to fetch new version. Rolling back"
        logger WARN "curl failed to fetch new version. Rolling back"
        return 1
    fi
}
dl_changelog(){
    local mdbranch
    local file="CHANGELOG.md"
    [[ $branch == "stable" ]] && mdbranch="dzgui"
    [[ $branch == "testing" ]] && mdbranch="testing"
    local md="$url_prefix/${mdbranch}/$file"
    curl -Ls "$md" > "$state_path/$file"
}
toggle(){
    shift
    local context="$1"
    case "$context" in
        Toggle[[:space:]]release[[:space:]]branch)
            if [[ $branch == "stable" ]]; then
                branch="testing"
            else
                branch="stable"
            fi
            update_config
            download_new_version
            [[ $? -eq 1 ]] && return 1
            return 255
            ;;
        Toggle[[:space:]]mod[[:space:]]install[[:space:]]mode)
            if [[ -z $auto_install ]]; then
                staging_dir="/tmp"
                auto_install="1"
            else
                auto_install=""
            fi
            ;;
        Toggle[[:space:]]debug[[:space:]]mode)
            if [[ -z $debug ]]; then
                debug="1"
            else
                debug=""
            fi
            ;;
        Toggle[[:space:]]Steam/Flatpak)
            if [[ $preferred_client == "steam" ]]; then
                preferred_client="flatpak"
            else
                preferred_client="steam"
            fi
            ;;
        Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot)
            if [[ $fullscreen == "true" ]]; then
                fullscreen="false"
            else
                fullscreen="true"
            fi
            ;;
    esac
    update_config
    return 90

}
add_to_favs(){
    local record="$1"
    ip_list+=("$record")
    update_config
    logger INFO "Added the record $record to saved servers"
    printf "Added %s to saved servers" $record
    return 90
}
remove_from_history(){
    shift
    local record="$1"
    # remove ip from history file
    local hist_cache=$(< "$history_file")
    <<< "$hist_cache" grep -v "$record" > "$history_file"
    # update cache
    local cache=$(< "$_cache_history")
    local r=$(<<< "$record" awk -F: '{print $1":"$2"␞"$3}')
    <<< "$cache" grep -v -P "$r$" > "$_cache_history"
}
remove_from_favs(){
    local record="$1"
    for (( i=0; i<${#ip_list[@]}; ++i )); do
        if [[ ${ip_list[$i]} == "$record" ]]; then
            unset ip_list[$i]
            break
        fi
    done
    if [[ ${#ip_list[@]} -gt 0 ]]; then
        readarray -t ip_list < <(printf "%s\n" "${ip_list[@]}")
    fi
    update_config
    local r=$(<<< "$record" awk -F: '{print $1":"$2"␞"$3}')
    local cache="$(< "$_cache_my_servers")"
    <<< "$cache" grep -v -P "$r$" > $_cache_my_servers
    logger INFO "Removed the record $record from saved servers"
    echo "Removed $record from saved servers"
    return 90
}
update_favs_from_table(){
    local context="$1"
    local record="$2"
    if [[ $context =~ Remove  ]]; then
        remove_from_favs "$record"
        echo "Removed $record from saved servers"
    else
        add_to_favs "$record"
    fi
    return 0
}
update_config(){
    # handling for legacy files
    [[ -z $branch ]] && branch="stable"
    mv $config_file ${config_file}.old
    write_config > $config_file
    logger INFO "Updated config file at '$config_file'"
}
validate_ip(){
    local ip="$1"
    local res
    <<< "$ip" grep -qP '^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}:[0-9]+$' && echo 0 || echo 1
}
test_steam_api(){
    local key="$1"
    [[ -z $key ]] && return 1
    local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100&limit=10&key=$key"
    local code=$(curl -ILs "$url" | grep -E "^HTTP")
    [[ $code =~ 403 ]] && echo 1
    [[ $code =~ 200 ]] && echo 0
}
test_bm_api(){
    local key="$1"
    if [[ ! $key =~ ^[0-9]+$ ]]; then
        return
    fi
    [[ -z $key ]] && return 1
    local code=$(curl -ILs "$bm_api" \
        -H "Authorization: Bearer "$key"" -G \
        -d "filter[game]=$game" \
        | grep -E "^HTTP")
    [[ $code =~ 401 ]] && echo 1
    [[ $code =~ 200 ]] && echo 0
}
update_config_val(){
    local context="$1"
    local value="$2"
    case $1 in
        "Change player name")
            key="name"
            ;;
        "Change Steam API key")
            key="steam_api"
            if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then
                printf "Invalid API key"
                return 2
            fi
            ;;
        "Change Battlemetrics API key")
            key="api_key"
            if [[ $(test_bm_api "$value") -eq 1 ]]; then
                printf "Invalid API key"
                return 2
            fi
            ;;
    esac
    declare -n nr=$key
    nr="$value"
    update_config
    echo "Updated the key '$key' to '$value'"
    return 90
}
show_log(){
    < "$debug_log" sed 's/Keyword␞/Keyword/'
}
open_user_workshop(){
    shift
    local id="$1"
    url="https://steamcommunity.com/profiles/$id/myworkshopfiles/?appid=$aid&browsefilter=mysubscriptions"
    $steam_cmd steam://openurl/$url &
}
open_workshop_page(){
    shift
    local id="$1"
    local workshop_uri="steam://url/CommunityFilePage/$id"
    $steam_cmd "$workshop_uri" $id &
}
open_link(){
    shift
    local destination="$1"
    local url
    case "$destination" in
        "Open Battlemetrics")
            url="$battlemetrics_server_url"
            ;;
        "Open Steam API page")
            url="$steam_api_url"
            ;;
        "Open Battlemetrics API page")
            url="$battlemetrics_api_url"
            ;;
        "Documentation/help files (GitHub) ⧉")
            url="$help_url"
            ;;
        "Documentation/help files (Codeberg mirror) ⧉")
            url="$help_url2"
            ;;
        "Report a bug (GitHub) ⧉")
            url="$issues_url"
            ;;
        "DZGUI Subreddit ⧉")
            url="$forum_url"
            ;;
        "Sponsor (GitHub) ⧉")
            url="$sponsor_url"
            ;;
    esac

    #if [[ $is_steam_deck -eq 1 ]]; then
    #$steam_cmd steam://openurl/"$1" 2>/dev/null
    if [[ -n "$BROWSER" ]]; then
        logger INFO "Opening '$url' in '$BROWSER'"
        "$BROWSER" "$url"
    else
        logger INFO "Opening '$url' with xdg-open"
        xdg-open "$url"
    fi
}

quick_connect(){
    if [[ -z $fav_server ]]; then
        printf "No favorite server currently set"
        return 1
    fi
    try_connect "$fav_server"
}
connect_from_table(){
    shift
    local record="$1"
    try_connect  "$record"
}
pretty_print(){
    while read -r line; do
        printf "\t%s\n" "$line"
    done < "$@"
}
generate_log(){
    source $config_file
	cat <<-DOC > $system_log
	Distro: $(< /etc/os-release grep -w NAME | awk -F\" '{print $2}')
	Kernel: $(uname -mrs)
	CPU: $(< /proc/cpuinfo awk -F": " '/model name/ {print $2; exit}')
	Version: $version
	Branch: $branch
	Mode: $(if [[ -z $debug ]]; then echo normal; else echo debug; fi)
	Auto: $(if [[ -z $auto_install ]]; then echo normal; else echo auto; fi)
	Steam path: $steam_path
	Workshop path: $workshop_dir
	Game path: $game_dir
	Servers:
$(print_ip_list | sed 's/"//g')
	Mods:
$(list_mods | sed 's/^/\t/g')
	DOC
    printf "Wrote system log to %s" "$system_log"
    return 0
}
query_defunct(){
    readarray -t modlist <<< "$@"
    local 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'
    }
    local result=$(post | jq -r '
    .[].publishedfiledetails[]
    | select(.result==1)
    | select(.filename|contains("screenshot")|not)
    | "\(.file_size) \(.publishedfileid)"')
    <<< "$result" awk '{print $2}'
}
encode(){
    echo "$1" | md5sum | cut -c -8
}
compare(){
    local modlist="$@"
    diff=$(comm -23 <(<<< "$modlist" sort -u) <(installed_mods | sort))
    echo "$diff"
}
legacy_symlinks(){
    logger INFO "Removing legacy symlinks"

    readarray -t stale_mod_dirs1 < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type l -name '@?*-*')
    logger INFO "Read local mods into array 1 with length: ${#stale_mod_dirs[@]}"
    if [[ ${#stale_mod_dirs1} -ne 0 ]]; then
        for d in "${stale_mod_dirs1[@]}"; do
            unlink "$game_dir/$d"
        done
    fi

    readarray -t stale_mod_dirs2 < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type l -name '@??')
    logger INFO "Read local mods into array 2 with length: ${#stale_mod_dirs[@]}"
    if [[ ${#stale_mod_dirs2} -eq 0 ]]; then
        for d in "${stale_mod_dirs2[@]}"; do
            unlink "$game_dir/$d"
        done
    fi
}
symlinks(){
    _merge(){
        comm -23 <(printf "%s\n" "${mods[@]}" | sort) <(printf "%s\n" "${targets[@]}" | sort)
    }
    _pulse(){
        zenity --pulsate --progress --auto-close --no-cancel --title="DZGUI"
    }
    _create_links(){
        local arr=("$@")
        local encoded_id
        local link
        local mod
        for ((i=0; i<${#arr[@]}; i++)); do
            encoded_id=$(encode "${arr[$i]}")
            link="@$encoded_id"
            mod="${arr[$i]}"
            logger INFO "Creating link '$game_dir/$link' for '$workshop_dir/$mod'"
            [[ $STEAM_LAUNCH -eq 1 ]] && echo "# Creating mod link $((i+1))/${#arr[@]}"
            ln -s "$workshop_dir/$mod" "$game_dir/$link"
        done
    }

    readarray -t mods < <(find $workshop_dir -mindepth 1 -name meta.cpp | awk -F/ 'NF=NF-1{print $NF}')
    readarray -t links < <(find $game_dir -type l)

    if [[ ${#mods[@]} -eq 0 ]]; then
        logger INFO "No mods present, aborting"
        return
    fi

    if [[ ${#links[@]} -eq 0 ]]; then
        logger INFO "No symlinks present in '$game_dir', creating them"
        if [[ $STEAM_LAUNCH -eq 1 ]]; then
            _create_links "${mods[@]}" > >(_pulse)
        else
            _create_links "${mods[@]}"
        fi
        return
    fi

    readarray -t targets < <(printf "%s\n" "${links[@]}" | xargs readlink -f | awk -F/ '{print $NF}')
    readarray -t hits < <(_merge)

    if [[ ${#hits[@]} -eq 0 ]]; then
        logger INFO "Symlinks are up to date, skipping"
        return
    fi

    # update missing targets
    logger INFO "Found ${#hits[@]} unlinked mods"
    if [[ $STEAM_LAUNCH -eq 1 ]]; then
        _create_links "${hits[@]}" > >(_pulse)
    else
        _create_links "${hits[@]}"
    fi
}
update_history(){
    local record="$1"
    local old
    [[ -n $(grep "$record" $history_file) ]] && return
    if [[ -f $history_file ]]; then
        old=$(tail -n9 "$history_file")
        rm $history_file
        echo "$old" > "$history_file"
    fi
    echo "$record" >> "$history_file"
}
update_symlinks(){
    legacy_symlinks
    symlinks
}
try_fallback(){
    local ip="$1"
    local qport="$2"
    local mode="$3"
    if [[ $mode != "rules" ]] && [[ $mode != "names" ]]; then
        logger WARN "Fallback query API called with an invalid mode: '$mode'"
        return 1
    fi
    [[ -z $api_key ]] && return 1
    logger INFO "Using fallback query API on '$ip:$qport' with mode: '$mode'"
    local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
        -G -d "filter[game]=$game" \
        -d "filter[search]=%22${ip}:${qport}%22")
    [[ -z $res ]] && return 1
    local len=$(<<< "$res" jq '.data|length')
    [[ $len -eq 0 ]] && return 1

    # cull defunct entries
    local record=$(<<< "$res" jq -r '
        .data[].attributes
        |select(.status != "removed")')

    # reverse lookup gport
    local dlc
    local gport=$(<<< "$record" jq -r '.port')
    dlc=$(is_dlc "$ip" "$gport")
    if [[ $dlc == "true" ]]; then
        logger FAIL "Fallback query API called on DLC-enabled server"
        return 1
    fi

    case "$mode" in
        "rules")
            <<< "$record" jq '.details.modIds[]'
            ;;
        "names")
            <<< "$record" jq '
            .details|[.modNames, .modIds] as [$n, $i]
            | {names: $n, ids: $i}'
        ;;
    esac
}
try_connect(){
    local record="$1"
    local ip=$(<<< $record awk -F: '{print $1}')
    local gameport=$(<<< $record awk -F: '{print $2}')
    local qport=$(<<< $record awk -F: '{print $3}')
    local remote_mods
    remote_mods=$(a2s $ip $qport rules)
    if [[ $? -eq 1 ]]; then
        printf "Failed to fetch server modlist, possibly timed out"
        return 1
    fi
    logger INFO "Server returned modlist: $(<<< $remote_mods tr '\n' ' ')"
    local sanitized_mods=$(query_defunct "$remote_mods")
    local diff=$(compare "$sanitized_mods")

    logger INFO "Connection attempt for $ip:$qport"
    update_history "$record"

    if [[ -n $auto_install ]]; then
        logger INFO "Merging modlists"
        diff=$(merge_modlists "$diff")
        diff=$(query_defunct "$diff")
    fi
    if [[ -n $diff ]]; then
        if [[ $(check_architecture) -eq 1 ]] && [[ $(test_display_mode) == "gm" ]]; then
            printf "Use Desktop Mode to download mods on Steam Deck"
            return 1
        fi
        case $auto_install in
            "") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods";;
            1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" ;;
        esac
    else
        launch "$ip" "$gameport" "$sanitized_mods"
    fi
}
check_architecture(){
    local cpu=$(< /proc/cpuinfo grep "AMD Custom APU 0405")
    if [[ -n "$cpu" ]]; then
        echo 1
    else
        echo 0
    fi
}
focus_beta_client(){
    _wid(){
        wmctrl -ilx |\
            awk 'tolower($3) == "steamwebhelper.steam"' |\
            awk '$5 ~ /^Steam|Steam Games List/' |\
            awk '{print $1}'
        }
    $steam_cmd steam://open/library 2>/dev/null 1>&2 &&
    $steam_cmd steam://open/console 2>/dev/null 1>&2 &&
    sleep 1s
    until [[ -n $(_wid) ]]; do
        sleep 0.1s
    done
    wmctrl -ia $(_wid)
    sleep 0.1s

    local wid=$(xdotool getactivewindow)
    local geo=$(xdotool getwindowgeometry $wid)
    local pos=$(<<< "$geo" awk 'NR==2 {print $2}' | sed 's/,/ /')
    local dim=$(<<< "$geo" awk 'NR==3 {print $2}' | sed 's/x/ /')
    local pos1=$(<<< "$pos" awk '{print $1}')
    local pos2=$(<<< "$pos" awk '{print $2}')
    local dim1=$(<<< "$dim" awk '{print $1}')
    local dim2=$(<<< "$dim" awk '{print $2}')
    local dim1=$(((dim1/2)+pos1))
    local dim2=$(((dim2/2)+pos2))

    xdotool mousemove $dim1 $dim2
    xdotool click 1
    $steam_cmd steam://open/library 2>/dev/null 1>&2 &&
    $steam_cmd steam://open/console 2>/dev/null 1>&2
}
auto_mod_install(){
    # currently unused, merged with manual method
    local ip="$1"
    local gameport="$2"
    local diff="$3"
    local sanitized_mods="$4"

    console_dl "$diff" &&
    $steam_cmd steam://open/downloads

    local total=$(<<< "$diff" wc -l)
    until [[ -z $(compare "$diff") ]]; do
        local missing=$(compare "$diff" | wc -l)
        echo "# Downloaded $(($total-missing)) of $total mods. ESC cancels"
    done | $steamsafe_zenity --pulsate --progress --title="DZG Watcher" --auto-close --no-cancel --width=500 2>/dev/null
    if [[ ! $? -eq 0 ]]; then
        echo "User aborted connect process. Steam may have mods pending for download."
        exit 1
    fi

    local diff=$(compare "$sanitized_mods")

    if [[ -z $diff ]]; then
        #wipe old version file and replace with latest stamps
        rm "$versions_file"
        check_timestamps
        logger INFO "Local modlist matches remote, initiating launch request"
        launch "$ip" "$gameport" "$sanitized_mods"
    fi
}
force_update(){
    if [[ ! $auto_install -eq 1 ]]; then
        printf "Only available when mod auto-install is ON"
        return 1
    fi
    rm "$versions_file"
    local update=$(check_timestamps)
    manual_mod_install "null" "null" "$update" "null" "force"
    echo "Finished requesting mod updates."
    return 0
}
console_dl(){
    readarray -t modids <<< "$@"
    focus_beta_client
    sleep 1.5s
    for i in "${modids[@]}"; do
        xdotool type --delay 0 "workshop_download_item $aid $i"
        sleep 0.5s
        xdotool key Return
        sleep 0.5s
    done
}
get_local_stamps(){
    readarray -t modlist < <(printf "%s\n" "$@")
    local max="${#modlist[@]}"
    _concat(){
        for ((i=0;i<$max;i++)); do
            printf "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'
        }

    _post
}
update_stamps(){
    readarray -t stamps <<< "$1"
    for((i=0;i<${#stamps[@]};i++)); do
        printf "%s\n" "${stamps[$i]}" >> $versions_file
    done
}
check_timestamps(){
    readarray -t local_modlist < <(ls -1 $workshop_dir)
    local max=${#local_modlist[@]}
    logger INFO "Local mod count: $max"
    [[ $max -eq 0 ]] && return 1
    local local_stamps=$(get_local_stamps "${local_modlist[@]}")
    if [[ -z $local_stamps ]]; then
        logger WARN "Timestamp query returned empty response"
        return 1
    fi
    local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
    readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
    readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
    readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
    readarray -t old_times < <(< $versions_file awk -F, '{print $2}')

    if [[ ! -f $versions_file ]]; then
        logger INFO "No prior versions file found, creating"
        update_stamps "$aligned"
        #force refresh all mods if versions file was missing
        printf "%s\n" "${remote_ids[@]}"
        return 0
    fi

    declare -A remote_version
    declare -A local_version

    for((i = 0; i < ${#remote_ids[@]}; ++i)); do
        remote_version[${remote_ids[$i]}]=${remote_times[$i]}
    done

    needs_update=()
    for((i=0;i<${#old_ids[@]};i++)); do
        local id=${old_ids[$i]}
        local time=${old_times[$i]}
        if [[ $time != ${remote_version[$id]} ]]; then
                logger WARN "Mod '$id' timestamp '$time' != '${remote_version[$id]}'"
                needs_update+=($id)
        fi
    done
    printf "%s\n" "${needs_update[@]}"
}
merge_modlists(){
    local diff="$1"
    _sort(){
        printf "%s\n" "$diff"
        printf "%s\n" "${needs_update[@]}"
    }
    readarray -t needs_update < <(check_timestamps)
    if [[ -z ${needs_update[@]} ]]; then
        echo "$diff"
        return 0
    fi
    if [[ -z "$diff" ]] && [[ ${#needs_update[@]} -gt 0 ]]; then
        printf "%s\n" "${needs_update[@]}"
    else
        # remove duplicates
        _sort | sort -u
    fi
}
concat_mods(){
    readarray -t concat_arr <<< "$@"
    local id
    local encoded_id
    local link
    for i in "${concat_arr[@]}"; 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'
}
is_dayz_running(){
    local proc=$(ps aux | grep "DayZ_x64.exe" | grep -v grep)
    if [[ -n $proc ]]; then
        echo 1
    else
        echo 0
    fi
}
launch(){
    local ip="$1"
    local gameport="$2"
    local mods="$3"
    local concat
    if [[ -n $mods ]]; then
        concat=$(concat_mods "$mods")
    else
        concat=""
    fi

    update_symlinks
    if [[ $debug -eq 1 ]]; then
        local launch_options="$steam_cmd -applaunch $aid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
        printf "Debug mode: these options would have been used to launch the game: $launch_options"
        return 0
    fi
    echo "$concat" > "$_cache_launch"
    echo "$ip:$gameport" > "$_cache_address"
    logger INFO "Saved launch params: '$concat'"
    printf "Launch conditions satisfied. DayZ will now launch after you confirm this dialog."
    return 100
}
final_handshake(){
    local saved_mods=$(< "$_cache_launch")
    local saved_address=$(< "$_cache_address")
    local res=$(is_dayz_running)
    if [[ $res -eq 1 ]]; then
        logger WARN "DayZ appears to already be running"
        printf "Is DayZ already running? DZGUI cannot launch DayZ if another process is using it."
        return 1
    fi
    logger INFO "Kicking off Steam launch"
    local params=()
    params+=("-connect=$saved_address")
    params+=("-nolauncher")
    params+=("-nosplash")
    params+=("-skipintro")
    params+=("-name=$name")
    params+=("-mod=$saved_mods")
    $steam_cmd -applaunch $aid "${params[@]}" &
    until [[ $(is_dayz_running) -eq 1 ]]; do
        sleep 0.1s
    done
    logger INFO "Caught DayZ process"
    printf "\n"
    return 6
}
manual_mod_install(){
    local ip="$1"
    local gameport="$2"
    local diff="$3"
    local sanitized_mods="$4"
    local mode="$5"
    local ex="$state_path/dzg.watcher"

    readarray -t stage_mods <<< "$diff"
    [[ -f $ex ]] && rm $ex

    _watcher(){
        for((i=0;i<${#stage_mods[@]};i++)); do
            [[ -f $ex ]] && return 1
            log ${stage_mods[$i]}

            if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
                $steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
                echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress, you may be out of disk space."
            else
                $steam_cmd "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."
            fi
            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
            if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
                echo "# Steam is downloading ${stage_mods[$i]} (mod $((i+1)) of ${#stage_mods[@]}). You do not need to manually Subscribe."
            else
                echo "# Steam is downloading ${stage_mods[$i]} (mod $((i+1)) of ${#stage_mods[@]})"
            fi
            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 > >($steamsafe_zenity --pulsate --progress --auto-close --title="DZG Watcher" --width=500 2>/dev/null; rc=$?; [[ $rc -eq 1 ]] && touch $ex)

    if [[ $mode == "force" ]]; then
        rm "$versions_file"
        check_timestamps
        return 0
    fi

    local diff=$(compare "$sanitized_mods")
    if [[ -z $diff ]]; then
        if [[ $mode == "auto" ]]; then
            rm "$versions_file"
            check_timestamps
        fi
        launch "$ip" "$gameport" "$sanitized_mods"
    else
        printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
        exit 1
    fi
}
test_display_mode(){
    pgrep -a gamescope | grep -q "generate-drm-mode"
    if [[ $? -eq 0 ]]; then
        echo gm
    else
        echo dm
    fi
}
foreground(){
    if [[ $(command -v wmctrl) ]]; then
        wmctrl -a "DZG Watcher"
    else
        local wid=$(xdotool search --name "DZG Watcher")
        xdotool windowactivate $wid
    fi
}
main(){
    local params="$(printf '"%s", ' "$@")"
    logger INFO "Received request from UI constructor with params [${params::-2}]"
    func=${funcs["$1"]}
    [[ -z $func ]] && return 1
    if [[ -z $2 ]]; then
        shift
        $func
    else
        $func "$@"
    fi
}
main "$@"