Compare commits
11 Commits
b2f6abd667
...
2228cc26dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2228cc26dd | ||
|
|
9913e48524 | ||
|
|
a2dc935ffc | ||
|
|
1f9159792d | ||
|
|
d0d05cfc2b | ||
|
|
8578cad702 | ||
|
|
b1cca7bb69 | ||
|
|
a7486ccea9 | ||
|
|
ff678dccaf | ||
|
|
243e92b660 | ||
|
|
51f69e8b4e |
3
.gitignore
vendored
@ -1,4 +1 @@
|
|||||||
tags
|
tags
|
||||||
__pycache__
|
|
||||||
pre-commit
|
|
||||||
tests/
|
|
||||||
|
|||||||
105
CHANGELOG.md
@ -1,110 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
|
||||||
## Fixed
|
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
|
||||||
- Raise error correctly from module
|
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
|
||||||
## Changed
|
|
||||||
- Optimize time complexity of startup checks
|
|
||||||
- Optimize coordinate calculation by using local records first
|
|
||||||
- Rename some internal functions
|
|
||||||
- Wrap entire startup process in dialog
|
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
|
||||||
## Fixed
|
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
|
||||||
## Added
|
|
||||||
- Support DayZ Experimental
|
|
||||||
- Ping display on server tables
|
|
||||||
- Save descriptive text notes on a per-server basis
|
|
||||||
- Speed up load time and navigation of server tables
|
|
||||||
- More robust threading and cache system when filtering servers
|
|
||||||
- Show additional DayZ client information in Options menu
|
|
||||||
- Warn user of DayZ client version mismatches
|
|
||||||
- Support clickable hyperlinks in dialogs
|
|
||||||
- Rich server metadata details dialog (right-click on server table)
|
|
||||||
- Filter by modded servers
|
|
||||||
- Breadcrumbs showing current menu context
|
|
||||||
- Dedicated changelog page
|
|
||||||
- Dedicated keybindings page
|
|
||||||
- Dedicated settings page ("Options")
|
|
||||||
- Support sandboxed version of Flatpak
|
|
||||||
- Vim-style navigation keybindings
|
|
||||||
- Added "Return to main menu" button to dialog windows when failing to load server table
|
|
||||||
- Dynamic context menus for modded servers
|
|
||||||
- Additional keybindings for new filter toggles
|
|
||||||
- If a server is locked, prompt user when connecting
|
|
||||||
- Internal flag to allow distro-packaged releases to disable in-app updates
|
|
||||||
- Pre-boot checks to test whether script was invoked directly
|
|
||||||
- Commandline usage help text (GaryBlackbourne)
|
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
|
||||||
- Normalized buttons in dialogs and restored proper padding
|
|
||||||
- GTK errors being emitted to stdout when inserting debug table
|
|
||||||
- Entry dialog sensitivity when validating API keys
|
|
||||||
- Additional validation on entry dialogs to prevent submitting empty text
|
|
||||||
- Suppress errors during pre-boot checks when mods are not installed
|
|
||||||
- Centered filter checkboxes within panel
|
|
||||||
- Do not pop unhighlight/select stale buttons if no stale mods exist
|
|
||||||
- Improved keybinding interaction with side panels
|
|
||||||
- Suppress typeahead search in mod dialogs
|
|
||||||
- Do not trigger global API cooldown if no LAN/favorite servers are found
|
|
||||||
- Fix table column expansion in server mod dialogs
|
|
||||||
- Missing parameters when closing application via window decorations
|
|
||||||
- Statusbar contents not updating on certain pages
|
|
||||||
- Prevent debug button from activating when in certain input fields
|
|
||||||
- Window size not updating correctly when unmaximizing window after changing "fullscreen at boot" setting
|
|
||||||
- Branch toggle signal being emitted when entering Options menu from other contexts
|
|
||||||
- Mods rarely not appearing in local mod list if download completed too quickly
|
|
||||||
- Statusbar not updating when clicking a row after spamming keyboard input
|
|
||||||
- Extraneous logs being generated when subscribing to mods
|
|
||||||
- Narrow width of columns in modlist dialogs occluding text
|
|
||||||
- Newline terminators in history file
|
|
||||||
- Floating point number calculation
|
|
||||||
- Window resizing too small if no prior resolution was set
|
|
||||||
- ESC key destroying wait dialogs while a thread is pending
|
|
||||||
- Tooltip signals being erroneously emitted on main menu
|
|
||||||
- UI not being constructed correctly if CHANGELOG.md was missing
|
|
||||||
- Config file erroneously getting updated when populating settings menu
|
|
||||||
- Leaky variable name in dialog titles
|
|
||||||
- Prevent extraneous signals from propagating when column width is adjusted
|
|
||||||
- Server filter panel not being hidden when entering other page contexts
|
|
||||||
- Path to remote changelog being constructed incorrectly
|
|
||||||
- Dialogs with newlines breaking output in log table
|
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- Require Python 3.13
|
|
||||||
- Reduced global API cooldown from 60s to 30s
|
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
|
||||||
- Auto-focus first item when opening context menus
|
|
||||||
- Opacity setting on side buttons when in a different context
|
|
||||||
- Refactored BM API key validation to account for new key format
|
|
||||||
- State file serialization methods
|
|
||||||
- Optimize time complexity of pre-boot checks (GaryBlackbourne)
|
|
||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
|
||||||
|
|
||||||
## Dropped
|
|
||||||
- Ping readout in statusbar
|
|
||||||
- Extraneous information from right statusbar
|
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
|
||||||
## Fixed
|
|
||||||
- Normalize checksum numbers and dates
|
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
|
||||||
## Fixed
|
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
## Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
|
|||||||
10
README.md
@ -11,15 +11,6 @@ This overcomes certain limitations in the Linux client and helps prepare the gam
|
|||||||
- Concatenate launch options to pass to Steam
|
- Concatenate launch options to pass to Steam
|
||||||
- Connect to mod-enabled LAN servers
|
- Connect to mod-enabled LAN servers
|
||||||
|
|
||||||
## Design philosophy
|
|
||||||
|
|
||||||
DZGUI follows a specific set of guidelines, enumerated below:
|
|
||||||
|
|
||||||
- Provide a free and open-source tool under GPL
|
|
||||||
- Do not use undocumented, third-party servers to aggregate data; no central "DZGUI" server
|
|
||||||
- All connections go to Steam and DayZ servers directly; the tool should function in perpetuity without intermediary endpoints
|
|
||||||
- Target Linux desktop and Steam deck specifically; not a cross-platform tool
|
|
||||||
|
|
||||||
## Setup and usage
|
## Setup and usage
|
||||||
|
|
||||||
Refer to the documentation for installation and setup instructions:
|
Refer to the documentation for installation and setup instructions:
|
||||||
@ -27,6 +18,7 @@ Refer to the documentation for installation and setup instructions:
|
|||||||
- [GitHub](https://aclist.github.io/dzgui/index.html)
|
- [GitHub](https://aclist.github.io/dzgui/index.html)
|
||||||
- [Mirror (Codeberg)](https://aclist.codeberg.page)
|
- [Mirror (Codeberg)](https://aclist.codeberg.page)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|||||||
356
dzgui.sh
@ -1,8 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
src_path="$(readlink -e "$0")"
|
version=5.8.1
|
||||||
version=6.0.2
|
|
||||||
|
|
||||||
#CONSTANTS
|
#CONSTANTS
|
||||||
aid=221100
|
aid=221100
|
||||||
@ -40,7 +39,6 @@ cols_file="$state_path/$prefix.cols.json"
|
|||||||
|
|
||||||
#CACHE FILES
|
#CACHE FILES
|
||||||
coords_file="$cache_path/$prefix.coords"
|
coords_file="$cache_path/$prefix.coords"
|
||||||
src_path_file="$cache_path/$prefix.src"
|
|
||||||
|
|
||||||
#legacy paths
|
#legacy paths
|
||||||
hist_file="$config_path/history"
|
hist_file="$config_path/history"
|
||||||
@ -65,14 +63,17 @@ testing_url="$url_prefix/testing"
|
|||||||
releases_url="https://github.com/$author/$repo/releases/download/browser"
|
releases_url="https://github.com/$author/$repo/releases/download/browser"
|
||||||
km_helper_url="$releases_url/latlon"
|
km_helper_url="$releases_url/latlon"
|
||||||
|
|
||||||
|
|
||||||
set_im_module(){
|
set_im_module(){
|
||||||
#TODO: drop pending SteamOS changes
|
#TODO: drop pending SteamOS changes
|
||||||
if pgrep -a gamescope | grep -q "generate-drm-mode"; then
|
pgrep -a gamescope | grep -q "generate-drm-mode"
|
||||||
unset GTK_IM_MODULE
|
if [[ $? -eq 0 ]]; then
|
||||||
|
GTK_IM_MODULE=""
|
||||||
logger INFO "Detected Steam Deck (Game Mode), unsetting GTK_IM_MODULE"
|
logger INFO "Detected Steam Deck (Game Mode), unsetting GTK_IM_MODULE"
|
||||||
|
else
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
redact(){
|
redact(){
|
||||||
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
||||||
}
|
}
|
||||||
@ -86,21 +87,13 @@ logger(){
|
|||||||
printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" \
|
printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" \
|
||||||
| redact >> "$debug_log"
|
| redact >> "$debug_log"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_dirs(){
|
setup_dirs(){
|
||||||
directories=()
|
for dir in "$state_path" "$cache_path" "$share_path" "$helpers_path" "$freedesktop_path" "$config_path" "$log_path"; do
|
||||||
directories+=("$state_path")
|
if [[ ! -d $dir ]]; then
|
||||||
directories+=("$cache_path")
|
|
||||||
directories+=("$share_path")
|
|
||||||
directories+=("$helpers_path")
|
|
||||||
directories+=("$freedesktop_path")
|
|
||||||
directories+=("$config_path")
|
|
||||||
directories+=("$log_path")
|
|
||||||
for dir in "${directories[@]}"; do
|
|
||||||
mkdir -p "$dir"
|
mkdir -p "$dir"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_state_files(){
|
setup_state_files(){
|
||||||
if [[ -f "$debug_log" ]]; then
|
if [[ -f "$debug_log" ]]; then
|
||||||
rm "$debug_log" && touch $debug_log
|
rm "$debug_log" && touch $debug_log
|
||||||
@ -111,13 +104,13 @@ setup_state_files(){
|
|||||||
logger INFO "Migrating legacy version file"
|
logger INFO "Migrating legacy version file"
|
||||||
fi
|
fi
|
||||||
# wipe cache files
|
# wipe cache files
|
||||||
if [[ $(ls -A "$cache_path") ]]; then
|
local path="$cache_path"
|
||||||
for file in "$cache_path"/*; do
|
if find "$path" -mindepth 1 -maxdepth 1 | read; then
|
||||||
|
for file in $path/*; do
|
||||||
rm "$file"
|
rm "$file"
|
||||||
done
|
done
|
||||||
logger INFO "Wiped cache files"
|
logger INFO "Wiped cache files"
|
||||||
fi
|
fi
|
||||||
echo "$src_path" > "$src_path_file"
|
|
||||||
}
|
}
|
||||||
print_config_vals(){
|
print_config_vals(){
|
||||||
local keys=(
|
local keys=(
|
||||||
@ -139,10 +132,10 @@ print_config_vals(){
|
|||||||
|
|
||||||
}
|
}
|
||||||
test_gobject(){
|
test_gobject(){
|
||||||
python3.13 -c "import gi"
|
python3 -c "import gi"
|
||||||
if [[ ! $? -eq 0 ]]; then
|
if [[ ! $? -eq 0 ]]; then
|
||||||
logger CRITICAL "Missing PyGObject"
|
logger CRITICAL "Missing PyGObject"
|
||||||
quit_with_help_dialog "Requires PyGObject (python-gobject)"
|
fdialog "Requires PyGObject (python-gobject)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
logger INFO "Found PyGObject in Python env"
|
logger INFO "Found PyGObject in Python env"
|
||||||
@ -221,14 +214,17 @@ default_steam_path="$default_steam_path"
|
|||||||
|
|
||||||
#Preferred Steam launch command (for Flatpak support)
|
#Preferred Steam launch command (for Flatpak support)
|
||||||
preferred_client="$preferred_client"
|
preferred_client="$preferred_client"
|
||||||
|
|
||||||
|
#DZGUI source path
|
||||||
|
src_path="$src_path"
|
||||||
END
|
END
|
||||||
}
|
}
|
||||||
depcheck(){
|
depcheck(){
|
||||||
for dep in "${!deps[@]}"; do
|
for dep in "${!deps[@]}"; do
|
||||||
if ! command -v "$dep" &> /dev/null; then
|
command -v "$dep" 2>&1>/dev/null
|
||||||
|
if [[ $? -eq 1 ]]; then
|
||||||
local msg="Requires $dep >= ${deps[$dep]}"
|
local msg="Requires $dep >= ${deps[$dep]}"
|
||||||
echo "$msg"
|
raise_error_and_quit "$msg"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
local jqmsg="jq must be compiled with support for oniguruma"
|
local jqmsg="jq must be compiled with support for oniguruma"
|
||||||
@ -237,31 +233,14 @@ depcheck(){
|
|||||||
[[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg"
|
[[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg"
|
||||||
logger INFO "Initial dependencies satisfied"
|
logger INFO "Initial dependencies satisfied"
|
||||||
}
|
}
|
||||||
open_url(){
|
|
||||||
url="$1"
|
|
||||||
if [[ -n "$BROWSER" ]]; then
|
|
||||||
logger INFO "Opening '$url' in '$BROWSER'"
|
|
||||||
"$BROWSER" "$url"
|
|
||||||
else
|
|
||||||
logger INFO "Opening '$url' with xdg-open"
|
|
||||||
xdg-open "$url"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
quit_with_help_dialog(){
|
|
||||||
local sel
|
|
||||||
msg="$1"
|
|
||||||
help_button="Open help page"
|
|
||||||
url="https://aclist.github.io/dzgui/installation.html"
|
|
||||||
logger CRITICAL "$msg"
|
|
||||||
sel=$(zenity --info --extra-button="$help_button" --text="$msg")
|
|
||||||
[[ $sel == "$help_button" ]] && open_url "$url" &
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
check_pyver(){
|
check_pyver(){
|
||||||
if [[ ! $(python3.13 --version) ]]; then
|
local pyver=$(python3 --version | awk '{print $2}')
|
||||||
local msg="Requires Python 3.13"
|
local minor=$(<<< $pyver awk -F. '{print $2}')
|
||||||
quit_with_help_dialog "$msg"
|
if [[ -z $pyver ]] || [[ ${pyver:0:1} -lt 3 ]] || [[ $minor -lt 10 ]]; then
|
||||||
|
local msg="Requires Python >=3.10"
|
||||||
|
raise_error_and_quit "$msg"
|
||||||
fi
|
fi
|
||||||
|
logger INFO "Found Python version: $pyver"
|
||||||
}
|
}
|
||||||
watcher_deps(){
|
watcher_deps(){
|
||||||
if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then
|
if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then
|
||||||
@ -335,8 +314,6 @@ check_unmerged(){
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
check_version(){
|
check_version(){
|
||||||
echo "# Checking version"
|
|
||||||
[[ -n $reference_branch ]] && return
|
|
||||||
local version_url=$(format_version_url)
|
local version_url=$(format_version_url)
|
||||||
local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
|
local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
|
||||||
local res=$(get_response_code "$version_url")
|
local res=$(get_response_code "$version_url")
|
||||||
@ -400,11 +377,10 @@ prompt_dl(){
|
|||||||
dl_changelog(){
|
dl_changelog(){
|
||||||
local mdbranch
|
local mdbranch
|
||||||
local md
|
local md
|
||||||
source "$config_file"
|
|
||||||
[[ $branch == "stable" ]] && mdbranch="dzgui"
|
[[ $branch == "stable" ]] && mdbranch="dzgui"
|
||||||
[[ $branch == "testing" ]] && mdbranch="testing"
|
[[ $branch == "testing" ]] && mdbranch="testing"
|
||||||
local changelog="$url_prefix/${mdbranch}/CHANGELOG.md"
|
local md="$url_prefix/${mdbranch}/$file"
|
||||||
curl -Ls "$changelog" > "$state_path/CHANGELOG.md"
|
curl -Ls "$md" > "$state_path/CHANGELOG.md"
|
||||||
}
|
}
|
||||||
test_display_mode(){
|
test_display_mode(){
|
||||||
pgrep -a gamescope | grep -q "generate-drm-mode"
|
pgrep -a gamescope | grep -q "generate-drm-mode"
|
||||||
@ -415,7 +391,6 @@ test_display_mode(){
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
check_architecture(){
|
check_architecture(){
|
||||||
echo "# Checking system architecture"
|
|
||||||
local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}')
|
local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}')
|
||||||
read -a APU_MODEL <<< "$cpu"
|
read -a APU_MODEL <<< "$cpu"
|
||||||
if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then
|
if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then
|
||||||
@ -432,7 +407,6 @@ check_architecture(){
|
|||||||
logger INFO "Setting architecture to 'Steam Deck'"
|
logger INFO "Setting architecture to 'Steam Deck'"
|
||||||
}
|
}
|
||||||
check_map_count(){
|
check_map_count(){
|
||||||
echo "# Checking map count"
|
|
||||||
[[ $is_steam_deck -gt 0 ]] && return 0
|
[[ $is_steam_deck -gt 0 ]] && return 0
|
||||||
local map_count_file="/proc/sys/vm/max_map_count"
|
local map_count_file="/proc/sys/vm/max_map_count"
|
||||||
local min_count=1048576
|
local min_count=1048576
|
||||||
@ -482,7 +456,6 @@ tdialog(){
|
|||||||
$steamsafe_zenity --info --text="$1" "${zenity_flags[@]}"
|
$steamsafe_zenity --info --text="$1" "${zenity_flags[@]}"
|
||||||
}
|
}
|
||||||
steam_deps(){
|
steam_deps(){
|
||||||
echo "# Checking Steam"
|
|
||||||
local flatpak
|
local flatpak
|
||||||
local steam
|
local steam
|
||||||
[[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam)
|
[[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam)
|
||||||
@ -491,70 +464,56 @@ steam_deps(){
|
|||||||
local msg="Found neither Steam nor Flatpak Steam"
|
local msg="Found neither Steam nor Flatpak Steam"
|
||||||
raise_error_and_quit "$msg"
|
raise_error_and_quit "$msg"
|
||||||
exit 1
|
exit 1
|
||||||
|
elif [[ -n "$steam" ]] && [[ -n "$flatpak" ]]; then
|
||||||
|
[[ -n $preferred_client ]] && return 0
|
||||||
|
if [[ -z $preferred_client ]]; then
|
||||||
|
preferred_client="steam"
|
||||||
fi
|
fi
|
||||||
|
elif [[ -n "$steam" ]]; then
|
||||||
|
preferred_client="steam"
|
||||||
|
else
|
||||||
|
preferred_client="flatpak"
|
||||||
|
fi
|
||||||
|
update_config
|
||||||
|
logger INFO "Preferred client set to '$preferred_client'"
|
||||||
|
}
|
||||||
|
migrate_files(){
|
||||||
|
if [[ ! -f $config_path/dztuirc.oldapi ]]; then
|
||||||
|
cp $config_file $config_path/dztuirc.oldapi
|
||||||
|
logger INFO "Migrated old API file"
|
||||||
|
fi
|
||||||
|
[[ ! -f $hist_file ]] && return
|
||||||
|
rm $hist_file
|
||||||
|
logger INFO "Wiped old history file"
|
||||||
}
|
}
|
||||||
stale_symlinks(){
|
stale_symlinks(){
|
||||||
echo "# Cleaning stale symlinks"
|
|
||||||
local game_dir="$steam_path/steamapps/common/DayZ"
|
local game_dir="$steam_path/steamapps/common/DayZ"
|
||||||
readarray -t links < <(find "$game_dir" -xtype l)
|
for l in "$(find "$game_dir" -xtype l)"; do
|
||||||
for link in "${links[@]}"; do
|
logger DEBUG "Updating stale symlink '$l'"
|
||||||
logger DEBUG "Updating stale symlink '$link'"
|
unlink "$l"
|
||||||
unlink "$link"
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
check_availability() {
|
|
||||||
if [[ -z $1 ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
local url=$1
|
|
||||||
local timeout_sec="2"
|
|
||||||
if [[ $2 ]]; then
|
|
||||||
timeout_sec=$2
|
|
||||||
fi
|
|
||||||
if ! ping -w "$timeout_sec" "$url" > /dev/null 2>&1; then
|
|
||||||
logger WARN "Failed to reach $url, service may be down."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
local_latlon(){
|
local_latlon(){
|
||||||
echo "# Checking coordinates"
|
|
||||||
if [[ -z $(command -v dig) ]]; then
|
if [[ -z $(command -v dig) ]]; then
|
||||||
local url_ipecho="https://ipecho.net/plain"
|
local local_ip=$(curl -Ls "https://ipecho.net/plain")
|
||||||
if ! check_availability "ipecho.net"; then
|
|
||||||
logger WARN "Failed to get external ip address, ipecho.net service may be down."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
local local_ip=$(curl -Ls "$url_ipecho")
|
|
||||||
else
|
else
|
||||||
# TODO : implement checking remote
|
|
||||||
local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
|
local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
|
||||||
fi
|
fi
|
||||||
if calc_local_coords "$local_ip"; then
|
local url="http://ip-api.com/json/$local_ip"
|
||||||
return 0
|
local res=$(curl -Ls "$url" | jq -r '"\(.lat)\n\(.lon)"')
|
||||||
fi
|
|
||||||
local url_ip_api="http://ip-api.com/json/$local_ip"
|
|
||||||
if ! check_availability "ip-api.com"; then
|
|
||||||
logger WARN "Failed to get local coordinates, ip-api.com service may be down."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
local res=$(curl -Ls "$url_ip_api" | jq -r '"\(.lat)\n\(.lon)"')
|
|
||||||
if [[ -z "$res" ]]; then
|
if [[ -z "$res" ]]; then
|
||||||
logger WARN "Failed to get local coordinates"
|
logger WARN "Failed to get local coordinates"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo "$res" > "$coords_file"
|
echo "$res" > "$coords_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
lock(){
|
lock(){
|
||||||
echo "# Setting up lock file"
|
|
||||||
[[ ! -f $lock_file ]] && touch $lock_file
|
[[ ! -f $lock_file ]] && touch $lock_file
|
||||||
local pid=$(cat $lock_file)
|
local pid=$(cat $lock_file)
|
||||||
ps -p $pid -o pid= >/dev/null 2>&1
|
ps -p $pid -o pid= >/dev/null 2>&1
|
||||||
res=$?
|
res=$?
|
||||||
if [[ $res -eq 0 ]]; then
|
if [[ $res -eq 0 ]]; then
|
||||||
local msg="DZGUI is already running ($pid)"
|
local msg="DZGUI already running ($pid)"
|
||||||
raise_error_and_quit "$msg"
|
raise_error_and_quit "$msg"
|
||||||
elif [[ $pid == $$ ]]; then
|
elif [[ $pid == $$ ]]; then
|
||||||
:
|
:
|
||||||
@ -583,13 +542,13 @@ fetch_a2s(){
|
|||||||
logger INFO "Updated A2S helper to sha '$sha'"
|
logger INFO "Updated A2S helper to sha '$sha'"
|
||||||
}
|
}
|
||||||
fetch_dzq(){
|
fetch_dzq(){
|
||||||
local sum="0a334e1e144e76e560419d155435c91e"
|
local sum="9caed1445c45832f4af87736ba3f9637"
|
||||||
local file="$helpers_path/a2s/dayzquery.py"
|
local file="$helpers_path/a2s/dayzquery.py"
|
||||||
if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then
|
if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then
|
||||||
logger INFO "DZQ is current"
|
logger INFO "DZQ is current"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
local sha=a22a9f428cbe075d7dda62f78000296955eea92a
|
local sha=3088bbfb147b77bc7b6a9425581b439889ff3f7f
|
||||||
local author="yepoleb"
|
local author="yepoleb"
|
||||||
local repo="dayzquery"
|
local repo="dayzquery"
|
||||||
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
|
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
|
||||||
@ -626,12 +585,11 @@ fetch_helpers_by_sum(){
|
|||||||
[[ -f "$config_file" ]] && source "$config_file"
|
[[ -f "$config_file" ]] && source "$config_file"
|
||||||
declare -A sums
|
declare -A sums
|
||||||
sums=(
|
sums=(
|
||||||
["funcs"]="5523d933e9f0668fc14b3faf0d4aafd8"
|
["ui.py"]="f128a97e744e9e11036d707198feb8a8"
|
||||||
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
||||||
["servers.py"]="ed442c3aecf33f777d59dcf53650d263"
|
|
||||||
["ui.py"]="d09b9d8bed3854efd51377289786c5ac"
|
|
||||||
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
||||||
["pefile.py"]="b452974a84bff1d821872fcebf59e380"
|
["funcs"]="94287c75a5ae0a06a95dd439711e6fba"
|
||||||
|
["lan"]="c62e84ddd1457b71a85ad21da662b9af"
|
||||||
)
|
)
|
||||||
local author="aclist"
|
local author="aclist"
|
||||||
local repo="dztui"
|
local repo="dztui"
|
||||||
@ -650,10 +608,6 @@ fetch_helpers_by_sum(){
|
|||||||
realbranch="dzgui"
|
realbranch="dzgui"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n $reference_branch ]]; then
|
|
||||||
realbranch="$reference_branch"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for i in "${!sums[@]}"; do
|
for i in "${!sums[@]}"; do
|
||||||
file="$i"
|
file="$i"
|
||||||
sum="${sums[$i]}"
|
sum="${sums[$i]}"
|
||||||
@ -677,6 +631,7 @@ fetch_helpers_by_sum(){
|
|||||||
logger INFO "Updated '$full_path' to sum '$sum'"
|
logger INFO "Updated '$full_path' to sum '$sum'"
|
||||||
fi
|
fi
|
||||||
[[ $file == "funcs" ]] && chmod +x "$full_path"
|
[[ $file == "funcs" ]] && chmod +x "$full_path"
|
||||||
|
[[ $file == "lan" ]] && chmod +x "$full_path"
|
||||||
done
|
done
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -691,12 +646,15 @@ fetch_km_helper(){
|
|||||||
}
|
}
|
||||||
get_response_code(){
|
get_response_code(){
|
||||||
local url="$1"
|
local url="$1"
|
||||||
curl --connect-timeout 3 -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
||||||
|
}
|
||||||
|
raise_error_and_quit(){
|
||||||
|
echo "$1"
|
||||||
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch_ip_db(){
|
fetch_ip_db(){
|
||||||
parse_dl_url(){
|
parse_dl_url(){
|
||||||
curl --connect-timeout 3 -Ls "$url" \
|
curl -Ls "$url" \
|
||||||
| grep "csv.gz" \
|
| grep "csv.gz" \
|
||||||
| awk -F"['']" '{print $2}'
|
| awk -F"['']" '{print $2}'
|
||||||
}
|
}
|
||||||
@ -712,9 +670,6 @@ fetch_ip_db(){
|
|||||||
fetch(){
|
fetch(){
|
||||||
logger INFO "Triggering fetch routine"
|
logger INFO "Triggering fetch routine"
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local ip_file="${state_path}/ips.csv"
|
|
||||||
local base_file="${state_path}/${this_month}.csv.gz"
|
|
||||||
local extracted_file="${state_path}/${this_month}.csv"
|
|
||||||
|
|
||||||
curl -Ls "$url" > "$base_file"
|
curl -Ls "$url" > "$base_file"
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
@ -734,7 +689,7 @@ fetch_ip_db(){
|
|||||||
logger WARN "Abnormal exit while parsing IPs"
|
logger WARN "Abnormal exit while parsing IPs"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
rm "${state_path}/${this_month}.csv"
|
rm "${state_path}/${month}.csv"
|
||||||
|
|
||||||
readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}')
|
readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}')
|
||||||
if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then
|
if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then
|
||||||
@ -749,13 +704,16 @@ fetch_ip_db(){
|
|||||||
rm "$ip_file"
|
rm "$ip_file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$this_month" > "$month_file"
|
echo "$month" > "$month_file"
|
||||||
logger INFO "Wrote '$this_month' to stub '$month_file'"
|
logger INFO "Wrote '$month' to stub '$month_file'"
|
||||||
logger INFO "Updated '$ip_file'"
|
logger INFO "Updated '$ip_file'"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_remote(){
|
local url="https://db-ip.com/db/download/ip-to-city-lite"
|
||||||
local url="$1"
|
local month_file="${state_path}/.month"
|
||||||
|
local ip_file="${state_path}/ips.csv"
|
||||||
|
|
||||||
|
# test main url
|
||||||
local res=$(get_response_code "$url")
|
local res=$(get_response_code "$url")
|
||||||
if [[ $res -ne 200 ]]; then
|
if [[ $res -ne 200 ]]; then
|
||||||
logger WARN "Failed to retrieve remote resource: '$url' ($res)"
|
logger WARN "Failed to retrieve remote resource: '$url' ($res)"
|
||||||
@ -771,35 +729,30 @@ fetch_ip_db(){
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
logger INFO "Resolved download URL: '$dl_url'"
|
logger INFO "Resolved download URL: '$dl_url'"
|
||||||
fetch "$dl_url"
|
|
||||||
}
|
|
||||||
|
|
||||||
local url="https://db-ip.com/db/download/ip-to-city-lite"
|
local month=$(parse_dl_url_date "$dl_url")
|
||||||
month_file="${state_path}/.month"
|
local base_file="${state_path}/${month}.csv.gz"
|
||||||
this_month=$(date +%Y-%m)
|
local extracted_file="${state_path}/${month}.csv"
|
||||||
|
|
||||||
# no stub file
|
# no stub file
|
||||||
if [[ ! -f $month_file ]]; then
|
if [[ ! -f $month_file ]]; then
|
||||||
logger WARN "No stub file '$month_file' present"
|
logger WARN "No stub file '$month_file' present"
|
||||||
check_remote "$url"
|
fetch "$dl_url"
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# if stub is same date, abort
|
|
||||||
local last_month=$(< "$month_file")
|
|
||||||
if [[ $last_month == "$this_month" ]]; then
|
|
||||||
logger INFO "Local stub '$last_month' is identical to remote, skipping"
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# local needs update
|
# local needs update
|
||||||
logger WARN "Local stub '$last_month' does not match '$this_month'"
|
local last_month=$(< "$month_file")
|
||||||
check_remote "$url"
|
if [[ $last_month != "$month" ]]; then
|
||||||
|
logger WARN "Local stub '$last_month' does not match remote stub '$month'"
|
||||||
|
fetch "$dl_url"
|
||||||
return
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if stub is same date, abort
|
||||||
|
logger INFO "Local stub '$last_month' is identical to remote, skipping"
|
||||||
}
|
}
|
||||||
fetch_helpers(){
|
fetch_helpers(){
|
||||||
echo "# Checking helper files"
|
|
||||||
fetch_a2s
|
fetch_a2s
|
||||||
fetch_dzq
|
fetch_dzq
|
||||||
fetch_km_helper
|
fetch_km_helper
|
||||||
@ -898,7 +851,7 @@ file_picker(){
|
|||||||
}
|
}
|
||||||
find_library_folder(){
|
find_library_folder(){
|
||||||
local search_path="$1"
|
local search_path="$1"
|
||||||
readarray -t paths < <(python3.13 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \
|
readarray -t paths < <(python3 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \
|
||||||
| jq -r '.libraryfolders[]|select(.apps|has("221100")).path')
|
| jq -r '.libraryfolders[]|select(.apps|has("221100")).path')
|
||||||
if [[ ! $? -eq 0 ]]; then
|
if [[ ! $? -eq 0 ]]; then
|
||||||
logger WARN "Failed to parse Steam path using '$search_path'"
|
logger WARN "Failed to parse Steam path using '$search_path'"
|
||||||
@ -926,7 +879,6 @@ create_config(){
|
|||||||
unset default_steam_path
|
unset default_steam_path
|
||||||
unset steam_path
|
unset steam_path
|
||||||
|
|
||||||
preferred_client="steam"
|
|
||||||
while true; do
|
while true; do
|
||||||
local player_input="$($steamsafe_zenity \
|
local player_input="$($steamsafe_zenity \
|
||||||
--forms \
|
--forms \
|
||||||
@ -987,9 +939,8 @@ create_config(){
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
varcheck(){
|
varcheck(){
|
||||||
echo "# Checking config file"
|
|
||||||
local msg="Config file '$config_file' missing. Start first-time setup now?"
|
local msg="Config file '$config_file' missing. Start first-time setup now?"
|
||||||
local msg2="The Steam paths set in your config file appear to be invalid (DayZ was moved or uninstalled). Restart first-time setup now?"
|
local msg2="The Steam paths set in your config file appear to be invalid. Restart first-time setup now?"
|
||||||
if [[ ! -f $config_file ]]; then
|
if [[ ! -f $config_file ]]; then
|
||||||
qdialog "$msg" "Yes" "Exit"
|
qdialog "$msg" "Yes" "Exit"
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
@ -1012,16 +963,18 @@ varcheck(){
|
|||||||
create_config
|
create_config
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
if [[ $src_path != $(realpath "$0") ]]; then
|
||||||
|
src_path=$(realpath "$0")
|
||||||
|
update_config
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
is_dzg_downloading(){
|
is_dzg_downloading(){
|
||||||
echo "# Checking DayZ"
|
|
||||||
if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then
|
if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then
|
||||||
logger WARN "DayZ may be scheduling updates"
|
logger WARN "DayZ may be scheduling updates"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
is_steam_running(){
|
is_steam_running(){
|
||||||
echo "# Checking Steam"
|
|
||||||
local res=$(ps aux | grep "steamwebhelper" | grep -v grep)
|
local res=$(ps aux | grep "steamwebhelper" | grep -v grep)
|
||||||
if [[ -z $res ]]; then
|
if [[ -z $res ]]; then
|
||||||
logger WARN "Steam may not be running"
|
logger WARN "Steam may not be running"
|
||||||
@ -1034,7 +987,6 @@ get_response_code(){
|
|||||||
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
||||||
}
|
}
|
||||||
test_connection(){
|
test_connection(){
|
||||||
echo "# Testing connection"
|
|
||||||
declare -A hr
|
declare -A hr
|
||||||
local res1
|
local res1
|
||||||
local res2
|
local res2
|
||||||
@ -1068,8 +1020,6 @@ legacy_cols(){
|
|||||||
mv $cols_file.new $cols_file
|
mv $cols_file.new $cols_file
|
||||||
}
|
}
|
||||||
stale_mod_signatures(){
|
stale_mod_signatures(){
|
||||||
echo "# Checking stale mods"
|
|
||||||
[[ ! -f "$versions_file" ]] && return
|
|
||||||
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
|
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
|
||||||
if [[ -d $workshop_dir ]]; then
|
if [[ -d $workshop_dir ]]; then
|
||||||
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)
|
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)
|
||||||
@ -1082,48 +1032,18 @@ stale_mod_signatures(){
|
|||||||
|
|
||||||
}
|
}
|
||||||
create_new_links(){
|
create_new_links(){
|
||||||
echo "# Setting up symlinks"
|
|
||||||
"$func_helper" "update_symlinks"
|
"$func_helper" "update_symlinks"
|
||||||
}
|
}
|
||||||
calc_local_coords(){
|
|
||||||
local ip="$1"
|
|
||||||
[[ ! -f "$geo_helper" ]] && return 1
|
|
||||||
|
|
||||||
IFS="." read -ra split <<< "$ip"
|
|
||||||
[[ ${#split[@]} -ne 4 ]] && return 1
|
|
||||||
|
|
||||||
prefix="^${split[0]}.${split[1]}."
|
|
||||||
readarray -t res < <(grep "$prefix" "$geo_helper")
|
|
||||||
|
|
||||||
for address in "${res[@]}"; do
|
|
||||||
IFS="," read -ra fields <<< "$address"
|
|
||||||
upper="${fields[1]}"
|
|
||||||
IFS="." read -ra upper_digits <<< "$upper"
|
|
||||||
if [[ ${split[2]} -gt ${upper_digits[2]} ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [[ ${split[2]} -eq ${upper_digits[2]} ]]; then
|
|
||||||
if [[ ${split[3]} -gt ${upper_digits[3]} ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [[ ${split[3]} -eq ${upper_digits[3]} ]]; then
|
|
||||||
echo "${fields[-2]}" >> "$coords_file"
|
|
||||||
echo "${fields[-1]}" >> "$coords_file"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [[ ${split[2]} -lt ${upper_digits[2]} ]]; then
|
|
||||||
echo "${fields[-2]}" >> "$coords_file"
|
|
||||||
echo "${fields[-1]}" >> "$coords_file"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
initial_setup(){
|
initial_setup(){
|
||||||
|
setup_dirs
|
||||||
|
setup_state_files
|
||||||
|
depcheck
|
||||||
|
check_pyver
|
||||||
|
test_gobject
|
||||||
|
watcher_deps
|
||||||
check_architecture
|
check_architecture
|
||||||
test_connection
|
test_connection
|
||||||
fetch_helpers
|
fetch_helpers > >(pdialog "Checking helper files")
|
||||||
varcheck
|
varcheck
|
||||||
source "$config_file"
|
source "$config_file"
|
||||||
lock
|
lock
|
||||||
@ -1132,12 +1052,14 @@ initial_setup(){
|
|||||||
check_version
|
check_version
|
||||||
check_map_count
|
check_map_count
|
||||||
steam_deps
|
steam_deps
|
||||||
|
migrate_files
|
||||||
stale_symlinks
|
stale_symlinks
|
||||||
stale_mod_signatures
|
stale_mod_signatures
|
||||||
create_new_links
|
create_new_links
|
||||||
local_latlon
|
local_latlon
|
||||||
is_steam_running
|
is_steam_running
|
||||||
is_dzg_downloading
|
is_dzg_downloading
|
||||||
|
print_config_vals
|
||||||
}
|
}
|
||||||
uninstall(){
|
uninstall(){
|
||||||
_full(){
|
_full(){
|
||||||
@ -1181,82 +1103,22 @@ uninstall(){
|
|||||||
rm "$self"
|
rm "$self"
|
||||||
echo "Uninstall routine complete"
|
echo "Uninstall routine complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
usage(){
|
|
||||||
cat <<- EOM
|
|
||||||
DZGUI - Free and Open Source DayZ launcher
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
dzgui.sh [options]
|
|
||||||
|
|
||||||
Description:
|
|
||||||
|
|
||||||
When no option is provided, the script launches DZGUI.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-u, --uninstall:
|
|
||||||
Uninstalls the software
|
|
||||||
|
|
||||||
-v, --version:
|
|
||||||
Prints the version
|
|
||||||
|
|
||||||
-h, --help:
|
|
||||||
Prints this message
|
|
||||||
EOM
|
|
||||||
}
|
|
||||||
|
|
||||||
main(){
|
main(){
|
||||||
# setup zenity environment
|
local zenv=$(zenity --version 2>/dev/null)
|
||||||
local zenv=""
|
|
||||||
zenv=$(zenity --version 2>/dev/null)
|
|
||||||
[[ -z $zenv ]] && { echo "Requires zenity >= ${deps[$steamsafe_zenity]}"; exit 1; }
|
[[ -z $zenv ]] && { echo "Requires zenity >= ${deps[$steamsafe_zenity]}"; exit 1; }
|
||||||
|
if [[ $1 == "--uninstall" ]] || [[ $1 == "-u" ]]; then
|
||||||
# parse arguments
|
uninstall &&
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
"--uninstall" | "-u")
|
|
||||||
uninstall
|
|
||||||
exit 0
|
exit 0
|
||||||
# shift
|
fi
|
||||||
;;
|
|
||||||
"--version" | "-v")
|
|
||||||
echo $version
|
|
||||||
exit 0
|
|
||||||
# shift
|
|
||||||
;;
|
|
||||||
"--help" | "-h")
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
# shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unrecognized command!"
|
|
||||||
return 1
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
set_im_module
|
set_im_module
|
||||||
|
|
||||||
printf "Checking dependencies...\n"
|
printf "Initializing setup...\n"
|
||||||
setup_dirs
|
initial_setup
|
||||||
setup_state_files
|
|
||||||
depcheck
|
|
||||||
check_pyver
|
|
||||||
test_gobject
|
|
||||||
watcher_deps
|
|
||||||
|
|
||||||
initial_setup > >(pdialog "Initializing setup")
|
|
||||||
|
|
||||||
printf "All OK. Kicking off UI...\n"
|
printf "All OK. Kicking off UI...\n"
|
||||||
python3.13 "$ui_helper" "--init-ui" "$version" "$is_steam_deck"
|
python3 "$ui_helper" "--init-ui" "$version" "$is_steam_deck"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ $(basename "$0") == "dzgui.sh" ]]; then
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
||||||
#TODO: tech debt: cruddy handling for steam forking
|
#TODO: tech debt: cruddy handling for steam forking
|
||||||
[[ $? -eq 1 ]] && pkill -f dzgui.sh
|
[[ $? -eq 1 ]] && pkill -f dzgui.sh
|
||||||
fi
|
|
||||||
|
|||||||
548
helpers/funcs
@ -1,10 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
version="6.0.2"
|
version="5.8.0"
|
||||||
|
|
||||||
#CONSTANTS
|
#CONSTANTS
|
||||||
aid=221100
|
aid=221100
|
||||||
exp=1024020
|
|
||||||
game="dayz"
|
game="dayz"
|
||||||
app_name="dzgui"
|
app_name="dzgui"
|
||||||
app_name_upper="DZGUI"
|
app_name_upper="DZGUI"
|
||||||
@ -47,11 +46,9 @@ _cache_my_servers="$cache_dir/$prefix.my_servers"
|
|||||||
_cache_history="$cache_dir/$prefix.history"
|
_cache_history="$cache_dir/$prefix.history"
|
||||||
_cache_launch="$cache_dir/$prefix.launch_mods"
|
_cache_launch="$cache_dir/$prefix.launch_mods"
|
||||||
_cache_address="$cache_dir/$prefix.launch_address"
|
_cache_address="$cache_dir/$prefix.launch_address"
|
||||||
_cache_binary="$cache_dir/$prefix.binary"
|
|
||||||
_cache_coords="$cache_path/$prefix.coords"
|
_cache_coords="$cache_path/$prefix.coords"
|
||||||
_cache_cooldown="$cache_path/$prefix.cooldown"
|
_cache_cooldown="$cache_path/$prefix.cooldown"
|
||||||
_cache_lan="$cache_path/$prefix.lan"
|
_cache_lan="$cache_path/$prefix.lan"
|
||||||
_cache_src_path="$cache_path/$prefix.src"
|
|
||||||
|
|
||||||
#XDG
|
#XDG
|
||||||
freedesktop_path="$HOME/.local/share/applications"
|
freedesktop_path="$HOME/.local/share/applications"
|
||||||
@ -85,28 +82,38 @@ forum_url="https://old.reddit.com/r/dzgui"
|
|||||||
sponsor_url="$gh_prefix/sponsors/$author"
|
sponsor_url="$gh_prefix/sponsors/$author"
|
||||||
battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz"
|
battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz"
|
||||||
steam_api_url="https://steamcommunity.com/dev/apikey"
|
steam_api_url="https://steamcommunity.com/dev/apikey"
|
||||||
|
#TODO: update link in docs
|
||||||
battlemetrics_api_url="https://www.battlemetrics.com/developers"
|
battlemetrics_api_url="https://www.battlemetrics.com/developers"
|
||||||
bm_api="https://api.battlemetrics.com/servers"
|
bm_api="https://api.battlemetrics.com/servers"
|
||||||
|
|
||||||
steam_cmd="$preferred_client"
|
if [[ $preferred_client == "steam" ]]; then
|
||||||
|
steam_cmd="steam"
|
||||||
|
else
|
||||||
|
steam_cmd="flatpak run com.valvesoftware.Steam"
|
||||||
|
fi
|
||||||
|
|
||||||
declare -A funcs=(
|
declare -A funcs=(
|
||||||
["Highlight stale"]="find_stale_mods"
|
["Highlight stale"]="find_stale_mods"
|
||||||
|
["My servers"]="dump_servers"
|
||||||
["Change player name"]="update_config_val"
|
["Change player name"]="update_config_val"
|
||||||
["Change Steam API key"]="update_config_val"
|
["Change Steam API key"]="update_config_val"
|
||||||
["Change Battlemetrics API key"]="update_config_val"
|
["Change Battlemetrics API key"]="update_config_val"
|
||||||
["Change client"]="update_config_val"
|
|
||||||
["Change favorite server"]="add_record"
|
["Change favorite server"]="add_record"
|
||||||
|
["Quick-connect to favorite server"]="quick_connect"
|
||||||
["Add server by IP"]="add_record"
|
["Add server by IP"]="add_record"
|
||||||
["Add server by ID"]="add_record"
|
["Add server by ID"]="add_record"
|
||||||
["try_connect"]="try_connect"
|
["Connect by IP"]="validate_and_connect"
|
||||||
|
["Connect by ID"]="validate_and_connect"
|
||||||
|
["Connect from table"]="connect_from_table"
|
||||||
["find_id"]="find_id"
|
["find_id"]="find_id"
|
||||||
["toggle"]="toggle"
|
["toggle"]="toggle"
|
||||||
["Open link"]="open_link"
|
["Open link"]="open_link"
|
||||||
|
["filter"]="dump_servers"
|
||||||
|
["dump_servers"]="dump_servers"
|
||||||
|
["get_unique_maps"]="get_unique_maps"
|
||||||
["get_dist"]="get_dist"
|
["get_dist"]="get_dist"
|
||||||
["test_cooldown"]="test_cooldown"
|
["test_cooldown"]="test_cooldown"
|
||||||
["query_config"]="query_config"
|
["query_config"]="query_config"
|
||||||
["query_favorites"]="query_favorites"
|
|
||||||
["start_cooldown"]="start_cooldown"
|
["start_cooldown"]="start_cooldown"
|
||||||
["List installed mods"]="list_mods"
|
["List installed mods"]="list_mods"
|
||||||
["Delete selected mods"]="delete_local_mod"
|
["Delete selected mods"]="delete_local_mod"
|
||||||
@ -123,31 +130,21 @@ declare -A funcs=(
|
|||||||
["Remove from history"]="remove_from_history"
|
["Remove from history"]="remove_from_history"
|
||||||
["Force update local mods"]="force_update"
|
["Force update local mods"]="force_update"
|
||||||
["Resolve IP"]="resolve_ip"
|
["Resolve IP"]="resolve_ip"
|
||||||
["Handshake"]="handshake"
|
["Handshake"]="final_handshake"
|
||||||
["Handshake_EXP"]="handshake_exp"
|
|
||||||
["get_player_count"]="get_player_count"
|
["get_player_count"]="get_player_count"
|
||||||
|
["lan_scan"]="lan_scan"
|
||||||
["update_symlinks"]="update_symlinks"
|
["update_symlinks"]="update_symlinks"
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_symlinks(){
|
lan_scan(){
|
||||||
local path="$(< $_cache_binary)"
|
local port="$1"
|
||||||
path=$(dirname "$path")
|
local res
|
||||||
for dir in $(find $game_dir -type l); do
|
res=$("$lan_helper" "$port")
|
||||||
local link=$(basename $dir)
|
if [[ -z $res ]]; then
|
||||||
ln -sf "${dir}" "${path}/${link}"
|
printf "\n"
|
||||||
done
|
else
|
||||||
}
|
printf "%s\n" "$res"
|
||||||
handshake(){
|
|
||||||
final_handshake "$aid"
|
|
||||||
}
|
|
||||||
handshake_exp(){
|
|
||||||
final_handshake "$exp"
|
|
||||||
}
|
|
||||||
query_favorites(){
|
|
||||||
if [[ -z "${ip_list[@]}" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
printf "%s\n" "${ip_list[@]}"
|
|
||||||
}
|
}
|
||||||
find_stale_mods(){
|
find_stale_mods(){
|
||||||
local res
|
local res
|
||||||
@ -203,6 +200,44 @@ get_player_count(){
|
|||||||
printf "%s\n%s" "$players" "$queue"
|
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(){
|
map_id_to_ip(){
|
||||||
local id="$1"
|
local id="$1"
|
||||||
local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
|
local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
|
||||||
@ -268,10 +303,32 @@ add_record(){
|
|||||||
;;
|
;;
|
||||||
esac
|
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(){
|
start_cooldown(){
|
||||||
logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
|
logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
|
||||||
date +%s > $_cache_cooldown
|
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(){
|
is_dlc(){
|
||||||
local dlc
|
local dlc
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
@ -286,7 +343,7 @@ a2s(){
|
|||||||
local mode="$3"
|
local mode="$3"
|
||||||
logger INFO "Querying '$ip:$qport' with mode '$mode'"
|
logger INFO "Querying '$ip:$qport' with mode '$mode'"
|
||||||
local res
|
local res
|
||||||
res=$(python3.13 "$query_helper" "$ip" "$qport" "$mode")
|
res=$(python3 "$query_helper" "$ip" "$qport" "$mode")
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
res=$(try_fallback "$ip" "$qport" "$mode")
|
res=$(try_fallback "$ip" "$qport" "$mode")
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
@ -309,7 +366,7 @@ is_in_favs(){
|
|||||||
find_id(){
|
find_id(){
|
||||||
local file="$default_steam_path/config/loginusers.vdf"
|
local file="$default_steam_path/config/loginusers.vdf"
|
||||||
[[ ! -f $file ]] && return 1
|
[[ ! -f $file ]] && return 1
|
||||||
local res=$(python3.13 "$helpers_path/vdf2json.py" \
|
local res=$(python3 "$helpers_path/vdf2json.py" \
|
||||||
-i "$file" \
|
-i "$file" \
|
||||||
| jq -r '.users
|
| jq -r '.users
|
||||||
|to_entries[]
|
|to_entries[]
|
||||||
@ -338,7 +395,7 @@ list_mods(){
|
|||||||
name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp")
|
name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp")
|
||||||
base_dir=$(basename $(readlink -f $game_dir/$symlink))
|
base_dir=$(basename $(readlink -f $game_dir/$symlink))
|
||||||
size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}')
|
size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}')
|
||||||
size=$(python3.13 -c "n=($size/1024) +.005; print(round(n,4))")
|
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"
|
LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size"
|
||||||
done | sort -k1)
|
done | sort -k1)
|
||||||
# user may have manually pruned mods out-of-band
|
# user may have manually pruned mods out-of-band
|
||||||
@ -367,10 +424,6 @@ local_latlon(){
|
|||||||
get_dist(){
|
get_dist(){
|
||||||
shift
|
shift
|
||||||
local given_ip="$1"
|
local given_ip="$1"
|
||||||
if [[ ! -f $_cache_coords ]]; then
|
|
||||||
printf "Unknown"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
readarray -t coords < "$_cache_coords"
|
readarray -t coords < "$_cache_coords"
|
||||||
readarray -t n < <(<<< "$given_ip" awk 'BEGIN{RS="."}{$1=$1}1')
|
readarray -t n < <(<<< "$given_ip" awk 'BEGIN{RS="."}{$1=$1}1')
|
||||||
|
|
||||||
@ -393,11 +446,60 @@ get_dist(){
|
|||||||
else
|
else
|
||||||
logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'"
|
logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'"
|
||||||
local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
|
local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
|
||||||
LC_NUMERIC=C printf "%.0f" "$dist"
|
LC_NUMERIC=C printf "%d" "$dist"
|
||||||
logger INFO "Distance: $dist km"
|
logger INFO "Distance: $dist km"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
get_remote_servers(){
|
||||||
|
params=(
|
||||||
|
"\\nor\1\map\chernarusplus\\nor\1\map\sakhal\\nor\1\map\enoch\empty\1\\nor\1\map\namalsk"
|
||||||
|
"\map\namalsk\empty\1"
|
||||||
|
"\map\namalsk\noplayers\1"
|
||||||
|
"\map\chernarusplus\empty\1"
|
||||||
|
"\map\chernarusplus\noplayers\1"
|
||||||
|
"\map\\sakhal\empty\1"
|
||||||
|
"\map\\sakhal\noplayers\1"
|
||||||
|
"\map\\enoch\empty\1"
|
||||||
|
"\map\\enoch\noplayers\1"
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
# ubuntu variants do not support jq 1.7 chained operators
|
||||||
|
# https://github.com/jqlang/jq/releases/tag/jq-1.7
|
||||||
|
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(){
|
query_config(){
|
||||||
[[ -n $2 ]] && local key=$2
|
[[ -n $2 ]] && local key=$2
|
||||||
keys=(
|
keys=(
|
||||||
@ -408,7 +510,6 @@ query_config(){
|
|||||||
"fav_label"
|
"fav_label"
|
||||||
"preferred_client"
|
"preferred_client"
|
||||||
"fullscreen"
|
"fullscreen"
|
||||||
"default_steam_path"
|
|
||||||
)
|
)
|
||||||
if [[ -n $key ]]; then
|
if [[ -n $key ]]; then
|
||||||
if [[ -n ${!key} ]]; then
|
if [[ -n ${!key} ]]; then
|
||||||
@ -423,7 +524,154 @@ query_config(){
|
|||||||
echo "${!i}"
|
echo "${!i}"
|
||||||
done
|
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
|
||||||
|
}
|
||||||
|
filter_official(){
|
||||||
|
if [[ ${filters[*]} =~ Official ]]; then
|
||||||
|
echo -n "$filtered"
|
||||||
|
else
|
||||||
|
<<< "$filtered" awk -F$separator '$10 == "Community"'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
filter_community(){
|
||||||
|
if [[ ${filters[*]} =~ Unoffic ]]; then
|
||||||
|
echo -n "$filtered"
|
||||||
|
else
|
||||||
|
<<< "$filtered" awk -F$separator '$10 == "Official"'
|
||||||
|
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)
|
||||||
|
filtered=$(filter_official)
|
||||||
|
filtered=$(filter_community)
|
||||||
|
|
||||||
|
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])␞" +
|
||||||
|
"\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "external") then "Community" else "Official" end) end)"
|
||||||
|
' | sort -k1
|
||||||
|
}
|
||||||
align_versions_file(){
|
align_versions_file(){
|
||||||
shift
|
shift
|
||||||
local mod="$1"
|
local mod="$1"
|
||||||
@ -475,13 +723,69 @@ test_cooldown(){
|
|||||||
local old_time=$(< $_cache_cooldown)
|
local old_time=$(< $_cache_cooldown)
|
||||||
local cur_time=$(date +%s)
|
local cur_time=$(date +%s)
|
||||||
local delta=$(($cur_time - $old_time))
|
local delta=$(($cur_time - $old_time))
|
||||||
if [[ $delta -lt 30 ]]; then
|
if [[ $delta -lt 60 ]]; then
|
||||||
local remains=$((30 - $delta))
|
local remains=$((60 - $delta))
|
||||||
local suffix=$(pluralize "seconds" $remains)
|
local suffix=$(pluralize "seconds" $remains)
|
||||||
printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
|
printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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" "$@"
|
||||||
|
}
|
||||||
redact(){
|
redact(){
|
||||||
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
||||||
}
|
}
|
||||||
@ -589,6 +893,9 @@ default_steam_path="$default_steam_path"
|
|||||||
|
|
||||||
#Preferred Steam launch command (for Flatpak support)
|
#Preferred Steam launch command (for Flatpak support)
|
||||||
preferred_client="$preferred_client"
|
preferred_client="$preferred_client"
|
||||||
|
|
||||||
|
#DZGUI source path
|
||||||
|
src_path="$src_path"
|
||||||
END
|
END
|
||||||
}
|
}
|
||||||
format_version_url(){
|
format_version_url(){
|
||||||
@ -642,7 +949,6 @@ download_new_version(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
local version_url="$(format_version_url)"
|
local version_url="$(format_version_url)"
|
||||||
local src_path="$(< "$_cache_src_path")"
|
|
||||||
mv "$src_path" "$src_path.old"
|
mv "$src_path" "$src_path.old"
|
||||||
curl -Ls "$version_url" > "$src_path"
|
curl -Ls "$version_url" > "$src_path"
|
||||||
rc=$?
|
rc=$?
|
||||||
@ -699,6 +1005,13 @@ toggle(){
|
|||||||
debug=""
|
debug=""
|
||||||
fi
|
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)
|
Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot)
|
||||||
if [[ $fullscreen == "true" ]]; then
|
if [[ $fullscreen == "true" ]]; then
|
||||||
fullscreen="false"
|
fullscreen="false"
|
||||||
@ -782,10 +1095,10 @@ test_steam_api(){
|
|||||||
}
|
}
|
||||||
test_bm_api(){
|
test_bm_api(){
|
||||||
local key="$1"
|
local key="$1"
|
||||||
if [[ -z $key ]]; then
|
if [[ ! $key =~ ^[0-9]+$ ]]; then
|
||||||
echo 1
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
[[ -z $key ]] && return 1
|
||||||
local code=$(curl -ILs "$bm_api" \
|
local code=$(curl -ILs "$bm_api" \
|
||||||
-H "Authorization: Bearer "$key"" -G \
|
-H "Authorization: Bearer "$key"" -G \
|
||||||
-d "filter[game]=$game" \
|
-d "filter[game]=$game" \
|
||||||
@ -797,35 +1110,29 @@ update_config_val(){
|
|||||||
local context="$1"
|
local context="$1"
|
||||||
local value="$2"
|
local value="$2"
|
||||||
case $1 in
|
case $1 in
|
||||||
"Change client")
|
|
||||||
key="preferred_client"
|
|
||||||
;;
|
|
||||||
"Change player name")
|
"Change player name")
|
||||||
key="name"
|
key="name"
|
||||||
if [[ -z "${value// }" ]]; then
|
|
||||||
printf "Invalid name"
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
"Change Steam API key")
|
"Change Steam API key")
|
||||||
key="steam_api"
|
key="steam_api"
|
||||||
if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then
|
if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then
|
||||||
printf "Invalid API key"
|
printf "Invalid API key"
|
||||||
return 78
|
return 2
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"Change Battlemetrics API key")
|
"Change Battlemetrics API key")
|
||||||
key="api_key"
|
key="api_key"
|
||||||
if [[ $(test_bm_api "$value") -eq 1 ]]; then
|
if [[ $(test_bm_api "$value") -eq 1 ]]; then
|
||||||
printf "Invalid API key"
|
printf "Invalid API key"
|
||||||
return 79
|
return 2
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
declare -n nr=$key
|
declare -n nr=$key
|
||||||
nr="$value"
|
nr="$value"
|
||||||
update_config
|
update_config
|
||||||
return 80
|
echo "Updated the key '$key' to '$value'"
|
||||||
|
return 90
|
||||||
}
|
}
|
||||||
show_log(){
|
show_log(){
|
||||||
< "$debug_log" sed 's/Keyword␞/Keyword/'
|
< "$debug_log" sed 's/Keyword␞/Keyword/'
|
||||||
@ -883,6 +1190,24 @@ open_link(){
|
|||||||
xdg-open "$url"
|
xdg-open "$url"
|
||||||
fi
|
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(){
|
generate_log(){
|
||||||
source $config_file
|
source $config_file
|
||||||
cat <<-DOC > $system_log
|
cat <<-DOC > $system_log
|
||||||
@ -1023,9 +1348,6 @@ update_history(){
|
|||||||
echo "$record" >> "$history_file"
|
echo "$record" >> "$history_file"
|
||||||
}
|
}
|
||||||
update_symlinks(){
|
update_symlinks(){
|
||||||
if [[ ! -d "$workshop_dir" ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
legacy_symlinks
|
legacy_symlinks
|
||||||
symlinks
|
symlinks
|
||||||
}
|
}
|
||||||
@ -1072,17 +1394,10 @@ try_fallback(){
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
try_connect(){
|
try_connect(){
|
||||||
shift
|
|
||||||
local record="$1"
|
local record="$1"
|
||||||
local appid="$2"
|
|
||||||
local binary="$3"
|
|
||||||
|
|
||||||
local ip=$(<<< $record awk -F: '{print $1}')
|
local ip=$(<<< $record awk -F: '{print $1}')
|
||||||
local gameport=$(<<< $record awk -F: '{print $2}')
|
local gameport=$(<<< $record awk -F: '{print $2}')
|
||||||
local qport=$(<<< $record awk -F: '{print $3}')
|
local qport=$(<<< $record awk -F: '{print $3}')
|
||||||
|
|
||||||
[[ $appid -eq $exp ]] && echo "$binary" > $_cache_binary
|
|
||||||
|
|
||||||
local remote_mods
|
local remote_mods
|
||||||
remote_mods=$(a2s $ip $qport rules)
|
remote_mods=$(a2s $ip $qport rules)
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
@ -1107,11 +1422,11 @@ try_connect(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
case $auto_install in
|
case $auto_install in
|
||||||
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "" "$appid";;
|
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods";;
|
||||||
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" "$appid" ;;
|
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
launch "$ip" "$gameport" "$sanitized_mods"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
check_architecture(){
|
check_architecture(){
|
||||||
@ -1122,6 +1437,68 @@ check_architecture(){
|
|||||||
echo 0
|
echo 0
|
||||||
fi
|
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(){
|
force_update(){
|
||||||
if [[ ! $auto_install -eq 1 ]]; then
|
if [[ ! $auto_install -eq 1 ]]; then
|
||||||
printf "Only available when mod auto-install is ON"
|
printf "Only available when mod auto-install is ON"
|
||||||
@ -1129,10 +1506,21 @@ force_update(){
|
|||||||
fi
|
fi
|
||||||
rm "$versions_file"
|
rm "$versions_file"
|
||||||
local update=$(check_timestamps)
|
local update=$(check_timestamps)
|
||||||
manual_mod_install "null" "null" "$update" "null" "force" "null"
|
manual_mod_install "null" "null" "$update" "null" "force"
|
||||||
echo "Finished requesting mod updates."
|
echo "Finished requesting mod updates."
|
||||||
return 0
|
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(){
|
get_local_stamps(){
|
||||||
readarray -t modlist < <(printf "%s\n" "$@")
|
readarray -t modlist < <(printf "%s\n" "$@")
|
||||||
local max="${#modlist[@]}"
|
local max="${#modlist[@]}"
|
||||||
@ -1171,6 +1559,8 @@ check_timestamps(){
|
|||||||
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
|
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
|
||||||
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
|
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
|
||||||
readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
|
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
|
if [[ ! -f $versions_file ]]; then
|
||||||
logger INFO "No prior versions file found, creating"
|
logger INFO "No prior versions file found, creating"
|
||||||
@ -1180,9 +1570,6 @@ check_timestamps(){
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
|
|
||||||
readarray -t old_times < <(< $versions_file awk -F, '{print $2}')
|
|
||||||
|
|
||||||
declare -A remote_version
|
declare -A remote_version
|
||||||
declare -A local_version
|
declare -A local_version
|
||||||
|
|
||||||
@ -1225,9 +1612,6 @@ concat_mods(){
|
|||||||
local encoded_id
|
local encoded_id
|
||||||
local link
|
local link
|
||||||
for i in "${concat_arr[@]}"; do
|
for i in "${concat_arr[@]}"; do
|
||||||
until [[ -f $workshop_dir/$i/meta.cpp ]]; do
|
|
||||||
sleep 0.1s
|
|
||||||
done
|
|
||||||
id=$(awk -F"= " '/publishedid/ {print $2}' "$workshop_dir"/$i/meta.cpp | awk -F\; '{print $1}')
|
id=$(awk -F"= " '/publishedid/ {print $2}' "$workshop_dir"/$i/meta.cpp | awk -F\; '{print $1}')
|
||||||
encoded_id=$(encode $id)
|
encoded_id=$(encode $id)
|
||||||
link="@$encoded_id;"
|
link="@$encoded_id;"
|
||||||
@ -1246,8 +1630,6 @@ launch(){
|
|||||||
local ip="$1"
|
local ip="$1"
|
||||||
local gameport="$2"
|
local gameport="$2"
|
||||||
local mods="$3"
|
local mods="$3"
|
||||||
local appid="$4"
|
|
||||||
|
|
||||||
local concat
|
local concat
|
||||||
if [[ -n $mods ]]; then
|
if [[ -n $mods ]]; then
|
||||||
concat=$(concat_mods "$mods")
|
concat=$(concat_mods "$mods")
|
||||||
@ -1256,22 +1638,18 @@ launch(){
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
update_symlinks
|
update_symlinks
|
||||||
[[ $appid -eq $exp ]] && clone_symlinks
|
|
||||||
|
|
||||||
if [[ $debug -eq 1 ]]; then
|
if [[ $debug -eq 1 ]]; then
|
||||||
local launch_options="$steam_cmd -applaunch $appid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
|
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"
|
printf "Debug mode: these options would have been used to launch the game: $launch_options"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo "$concat" > "$_cache_launch"
|
echo "$concat" > "$_cache_launch"
|
||||||
echo "$ip:$gameport" > "$_cache_address"
|
echo "$ip:$gameport" > "$_cache_address"
|
||||||
logger INFO "Saved launch params: '$concat'"
|
logger INFO "Saved launch params: '$concat'"
|
||||||
printf "Launch conditions satisfied. DayZ will launch after you confirm this dialog."
|
printf "Launch conditions satisfied. DayZ will now launch after you confirm this dialog."
|
||||||
[[ $appid == "$aid" ]] && return 100
|
return 100
|
||||||
[[ $appid == "$exp" ]] && return 101
|
|
||||||
}
|
}
|
||||||
final_handshake(){
|
final_handshake(){
|
||||||
local appid="$1"
|
|
||||||
local saved_mods=$(< "$_cache_launch")
|
local saved_mods=$(< "$_cache_launch")
|
||||||
local saved_address=$(< "$_cache_address")
|
local saved_address=$(< "$_cache_address")
|
||||||
local res=$(is_dayz_running)
|
local res=$(is_dayz_running)
|
||||||
@ -1288,7 +1666,7 @@ final_handshake(){
|
|||||||
params+=("-skipintro")
|
params+=("-skipintro")
|
||||||
params+=("-name=$name")
|
params+=("-name=$name")
|
||||||
params+=("-mod=$saved_mods")
|
params+=("-mod=$saved_mods")
|
||||||
$steam_cmd -applaunch $appid "${params[@]}" &
|
$steam_cmd -applaunch $aid "${params[@]}" &
|
||||||
until [[ $(is_dayz_running) -eq 1 ]]; do
|
until [[ $(is_dayz_running) -eq 1 ]]; do
|
||||||
sleep 0.1s
|
sleep 0.1s
|
||||||
done
|
done
|
||||||
@ -1302,7 +1680,6 @@ manual_mod_install(){
|
|||||||
local diff="$3"
|
local diff="$3"
|
||||||
local sanitized_mods="$4"
|
local sanitized_mods="$4"
|
||||||
local mode="$5"
|
local mode="$5"
|
||||||
local appid="$6"
|
|
||||||
local ex="$state_path/dzg.watcher"
|
local ex="$state_path/dzg.watcher"
|
||||||
|
|
||||||
readarray -t stage_mods <<< "$diff"
|
readarray -t stage_mods <<< "$diff"
|
||||||
@ -1311,6 +1688,7 @@ manual_mod_install(){
|
|||||||
_watcher(){
|
_watcher(){
|
||||||
for((i=0;i<${#stage_mods[@]};i++)); do
|
for((i=0;i<${#stage_mods[@]};i++)); do
|
||||||
[[ -f $ex ]] && return 1
|
[[ -f $ex ]] && return 1
|
||||||
|
log ${stage_mods[$i]}
|
||||||
|
|
||||||
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
|
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
|
||||||
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
|
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
|
||||||
@ -1360,7 +1738,7 @@ manual_mod_install(){
|
|||||||
rm "$versions_file"
|
rm "$versions_file"
|
||||||
check_timestamps
|
check_timestamps
|
||||||
fi
|
fi
|
||||||
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
launch "$ip" "$gameport" "$sanitized_mods"
|
||||||
else
|
else
|
||||||
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
|
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
|
||||||
exit 1
|
exit 1
|
||||||
@ -1384,7 +1762,7 @@ foreground(){
|
|||||||
}
|
}
|
||||||
main(){
|
main(){
|
||||||
local params="$(printf '"%s", ' "$@")"
|
local params="$(printf '"%s", ' "$@")"
|
||||||
logger INFO "Received request from UI with params [${params::-2}]"
|
logger INFO "Received request from UI constructor with params [${params::-2}]"
|
||||||
func=${funcs["$1"]}
|
func=${funcs["$1"]}
|
||||||
[[ -z $func ]] && return 1
|
[[ -z $func ]] && return 1
|
||||||
if [[ -z $2 ]]; then
|
if [[ -z $2 ]]; then
|
||||||
|
|||||||
36
helpers/latlon.c
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
#define R 6371
|
||||||
|
#define TO_RAD (3.1415926536 / 180)
|
||||||
|
double dist(double th1, double ph1, double th2, double ph2)
|
||||||
|
{
|
||||||
|
double dx, dy, dz;
|
||||||
|
ph1 -= ph2;
|
||||||
|
ph1 *= TO_RAD, th1 *= TO_RAD, th2 *= TO_RAD;
|
||||||
|
|
||||||
|
dz = sin(th1) - sin(th2);
|
||||||
|
dx = cos(ph1) * cos(th1) - cos(th2);
|
||||||
|
dy = sin(ph1) * cos(th1);
|
||||||
|
return asin(sqrt(dx * dx + dy * dy + dz * dz) / 2) * 2 * R;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, const char * argv[])
|
||||||
|
{
|
||||||
|
if(argc < 5 || argc > 5){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
float coords[4];
|
||||||
|
for(int i=1;i<5;i++){
|
||||||
|
if(atof(argv[i]) == 0){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
coords[i] = atof(argv[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
double d = dist(coords[1], coords[2], coords[3], coords[4]);
|
||||||
|
printf("%.1f\n", d);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@ -1,492 +0,0 @@
|
|||||||
import json
|
|
||||||
import struct
|
|
||||||
import typing # noqa
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
|
||||||
from shlex import shlex
|
|
||||||
from typing import BinaryIO, Union
|
|
||||||
|
|
||||||
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
|
|
||||||
endian = "<"
|
|
||||||
IMAGE_DIRECTORY_ENTRY = 2
|
|
||||||
VERSION_RESOURCE = 16
|
|
||||||
RESOURCE_NODE = ".rsrc"
|
|
||||||
PE32_x86 = "0x10b"
|
|
||||||
PE32_x64 = "0x20b"
|
|
||||||
VS_VERSION_INFO_MAGIC = "0xfeef04bd0000"
|
|
||||||
VS_VERSION_INFO_ID = "VS_VERSION_INFO"
|
|
||||||
|
|
||||||
|
|
||||||
class VersionMatch(Enum):
|
|
||||||
LOCAL_OLDER = 1
|
|
||||||
LOCAL_NEWER = 2
|
|
||||||
SAME_VERSION = 3
|
|
||||||
|
|
||||||
|
|
||||||
class u8:
|
|
||||||
fmt = "B"
|
|
||||||
|
|
||||||
|
|
||||||
class u16:
|
|
||||||
fmt = "H"
|
|
||||||
|
|
||||||
|
|
||||||
class u32:
|
|
||||||
fmt = "L"
|
|
||||||
|
|
||||||
|
|
||||||
class u64:
|
|
||||||
fmt = "Q"
|
|
||||||
|
|
||||||
|
|
||||||
class i8:
|
|
||||||
fmt = "b"
|
|
||||||
|
|
||||||
|
|
||||||
class i16:
|
|
||||||
fmt = "h"
|
|
||||||
|
|
||||||
|
|
||||||
class i32:
|
|
||||||
fmt = "l"
|
|
||||||
|
|
||||||
|
|
||||||
class i64:
|
|
||||||
fmt = "q"
|
|
||||||
|
|
||||||
|
|
||||||
class PackedData:
|
|
||||||
@classmethod
|
|
||||||
def unpack(cls, data: BinaryIO):
|
|
||||||
r = []
|
|
||||||
for key, value in cls.__annotations__.items():
|
|
||||||
if value == str:
|
|
||||||
f = data.read(8).rstrip(b"\x00\x00").decode()
|
|
||||||
else:
|
|
||||||
fmt = endian + (value.fmt)
|
|
||||||
size = struct.calcsize(fmt)
|
|
||||||
f = struct.unpack(fmt, data.read(size))[0]
|
|
||||||
r.append(f)
|
|
||||||
return cls(*r)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class COFF_FILE_HDR(PackedData):
|
|
||||||
machine_type: u16
|
|
||||||
number_of_sections: u16
|
|
||||||
timestamp: u32
|
|
||||||
pointer_to_symbol_table: u32
|
|
||||||
number_of_symbols: u32
|
|
||||||
size_of_optional_header: u16
|
|
||||||
characteristics: u16
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class OPTIONAL_HDR_X86(PackedData):
|
|
||||||
magic: u16
|
|
||||||
major_linker_ver: u8
|
|
||||||
minor_linker_ver: u8
|
|
||||||
size_of_code: u32
|
|
||||||
size_of_initialized_data: u32
|
|
||||||
size_of_uninitialized_data: u32
|
|
||||||
address_of_entry_point: u32
|
|
||||||
base_of_code: u32
|
|
||||||
base_of_data: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class OPTIONAL_HDR_X64(PackedData):
|
|
||||||
magic: u16
|
|
||||||
major_linker_ver: u8
|
|
||||||
minor_linker_ver: u8
|
|
||||||
size_of_code: u32
|
|
||||||
size_of_initialized_data: u32
|
|
||||||
size_of_uninitialized_data: u32
|
|
||||||
address_of_entry_point: u32
|
|
||||||
base_of_code: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class OPTIONAL_HDR_WIN_X86(PackedData):
|
|
||||||
image_base: u32
|
|
||||||
section_alignment: u32
|
|
||||||
file_alignment: u32
|
|
||||||
major_operating_system_version: u16
|
|
||||||
minor_operating_system_version: u16
|
|
||||||
major_image_version: u16
|
|
||||||
minor_image_version: u16
|
|
||||||
major_subsystem_version: u16
|
|
||||||
minor_subsystem_version: u16
|
|
||||||
win32_version_value: u32
|
|
||||||
size_of_image: u32
|
|
||||||
size_of_headers: u32
|
|
||||||
checksum: u32
|
|
||||||
subsystem: u16
|
|
||||||
dll_characteristics: u16
|
|
||||||
size_of_stack_reserve: u32
|
|
||||||
size_of_stack_commit: u32
|
|
||||||
size_of_heap_reserve: u32
|
|
||||||
size_of_heap_commit: u32
|
|
||||||
loader_flags: u32
|
|
||||||
number_of_rva_and_sizes: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class OPTIONAL_HDR_WIN_X64(PackedData):
|
|
||||||
image_base: u64
|
|
||||||
section_alignment: u32
|
|
||||||
file_alignment: u32
|
|
||||||
major_operating_system_version: u16
|
|
||||||
minor_operating_system_version: u16
|
|
||||||
major_image_version: u16
|
|
||||||
minor_image_version: u16
|
|
||||||
major_subsystem_version: u16
|
|
||||||
minor_subsystem_version: u16
|
|
||||||
win32_version_value: u32
|
|
||||||
size_of_image: u32
|
|
||||||
size_of_headers: u32
|
|
||||||
checksum: u32
|
|
||||||
subsystem: u16
|
|
||||||
dll_characteristics: u16
|
|
||||||
size_of_stack_reserve: u64
|
|
||||||
size_of_stack_commit: u64
|
|
||||||
size_of_heap_reserve: u64
|
|
||||||
size_of_heap_commit: u64
|
|
||||||
loader_flags: u32
|
|
||||||
number_of_rva_and_sizes: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class DATA_DIR(PackedData):
|
|
||||||
virtual_address: u32
|
|
||||||
size: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class SECTION_HDR(PackedData):
|
|
||||||
name: str
|
|
||||||
virtual_size: u32
|
|
||||||
virtual_address: u32
|
|
||||||
size_of_raw_data: u32
|
|
||||||
pointer_to_raw_data: u32
|
|
||||||
pointer_to_relocations: u32
|
|
||||||
pointer_to_line_numbers: u32
|
|
||||||
number_of_relocations: u16
|
|
||||||
number_of_line_numbers: u16
|
|
||||||
characteristics: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class RESOURCE_DIRECTORY_TABLE(PackedData):
|
|
||||||
characteristics: u32
|
|
||||||
timestamp: u32
|
|
||||||
major_version: u16
|
|
||||||
minor_version: u16
|
|
||||||
number_of_name_entries: u16
|
|
||||||
number_of_id_entries: u16
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class RESOURCE_DIRECTORY_ENTRY(PackedData):
|
|
||||||
"""
|
|
||||||
This field is either a string identifying a data leaf
|
|
||||||
(if the high bit is set) or an ID to another nested directory
|
|
||||||
(if the high bit is clear). The outermost level is always a
|
|
||||||
directory. If it is a name, the lower 31 bits are the offset from the
|
|
||||||
beginning of the resource section's raw data to the name
|
|
||||||
(the name consists of 16 bits length and trailing wide characters,
|
|
||||||
in Unicode, not 0-terminated).
|
|
||||||
"""
|
|
||||||
|
|
||||||
name_or_id: u32
|
|
||||||
data_or_subdir: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class RESOURCE_DATA_ENTRY(PackedData):
|
|
||||||
data_rva: u32
|
|
||||||
size: u32
|
|
||||||
codepage: u32
|
|
||||||
reserved: u32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class VS_VERSION_INFO_HDR(PackedData):
|
|
||||||
size: u16
|
|
||||||
value_length: u16
|
|
||||||
value_type: u16
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class DayZVersion:
|
|
||||||
major: int
|
|
||||||
minor: int
|
|
||||||
patch: int
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class FileVersion:
|
|
||||||
major: int
|
|
||||||
minor: int
|
|
||||||
build: int
|
|
||||||
revision: int
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class Result:
|
|
||||||
local: Union[str, None]
|
|
||||||
remote: str
|
|
||||||
build: str
|
|
||||||
path: Union[Path, None]
|
|
||||||
match: VersionMatch
|
|
||||||
error: Union[Exception, None]
|
|
||||||
|
|
||||||
|
|
||||||
class PeFileError(Exception):
|
|
||||||
"""Expected contents missing from headers or resource nodes"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AppNotInstalledError(Exception):
|
|
||||||
"""App not present in user's libraryfolders"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AppMovedError(Exception):
|
|
||||||
"""VDF points to a nonexistent location on disk"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VDFLoadError(Exception):
|
|
||||||
"""Malformed VDF or JSON conversion"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def parse_version_number(data: BinaryIO):
|
|
||||||
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
|
||||||
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
|
||||||
major = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
|
||||||
build = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
|
||||||
revision = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
|
||||||
return FileVersion(major, minor, build, revision)
|
|
||||||
|
|
||||||
|
|
||||||
def seek_to_hex(address: str, data: BinaryIO) -> None:
|
|
||||||
data.seek(int(address, base=16))
|
|
||||||
|
|
||||||
|
|
||||||
def seek_to_pe_stub(data: BinaryIO) -> None:
|
|
||||||
MAGIC = "0x3c"
|
|
||||||
seek_to_hex(MAGIC, data)
|
|
||||||
e_lfanew = hex(struct.unpack("<h", data.read(2))[0])
|
|
||||||
seek_to_hex(e_lfanew, data)
|
|
||||||
pe_stub = data.read(4).rstrip(b"\x00\x00\x00\x00").decode()
|
|
||||||
if pe_stub != "PE":
|
|
||||||
raise PeFileError("missing PE header data")
|
|
||||||
|
|
||||||
|
|
||||||
def get_dayz_version(file: Path) -> DayZVersion | Exception:
|
|
||||||
try:
|
|
||||||
version = get_version(file)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
patch = str(version.build) + str(version.revision)
|
|
||||||
dz_vers = DayZVersion(version.major, version.minor, int(patch))
|
|
||||||
return dz_vers
|
|
||||||
|
|
||||||
|
|
||||||
def dayz_version_to_str(v: DayZVersion) -> str:
|
|
||||||
return ".".join(str(el) for el in [v.major, v.minor, v.patch])
|
|
||||||
|
|
||||||
|
|
||||||
def dayz_version_from_str(v: str) -> DayZVersion:
|
|
||||||
vers = v.split(".")
|
|
||||||
assert len(vers) == 3
|
|
||||||
return DayZVersion(*[int(el) for el in vers])
|
|
||||||
|
|
||||||
|
|
||||||
def get_version(file):
|
|
||||||
with open(file, "rb") as f:
|
|
||||||
seek_to_pe_stub(f)
|
|
||||||
COFF = COFF_FILE_HDR.unpack(f)
|
|
||||||
|
|
||||||
pos = f.tell()
|
|
||||||
blob = f.read()
|
|
||||||
magic = hex(struct.unpack("<H", (blob[0:2]))[0])
|
|
||||||
f.seek(pos)
|
|
||||||
|
|
||||||
if magic == PE32_x86:
|
|
||||||
OPTIONAL_HDR_X86.unpack(f)
|
|
||||||
OBJW = OPTIONAL_HDR_WIN_X86.unpack(f)
|
|
||||||
else:
|
|
||||||
OPTIONAL_HDR_X64.unpack(f)
|
|
||||||
OBJW = OPTIONAL_HDR_WIN_X64.unpack(f)
|
|
||||||
|
|
||||||
if OBJW.number_of_rva_and_sizes < 1:
|
|
||||||
raise PeFileError("no data resource directory")
|
|
||||||
|
|
||||||
data_dirs = []
|
|
||||||
for rva in range(OBJW.number_of_rva_and_sizes):
|
|
||||||
data_dir = DATA_DIR.unpack(f)
|
|
||||||
data_dirs.append(data_dir)
|
|
||||||
|
|
||||||
res_dir = data_dirs[IMAGE_DIRECTORY_ENTRY]
|
|
||||||
dir_va = res_dir.virtual_address
|
|
||||||
|
|
||||||
for section in range(COFF.number_of_sections):
|
|
||||||
hdr = SECTION_HDR.unpack(f)
|
|
||||||
if hdr.name == RESOURCE_NODE:
|
|
||||||
va = hdr.virtual_address
|
|
||||||
ptr = hdr.pointer_to_raw_data
|
|
||||||
offset = dir_va - va + ptr
|
|
||||||
seek_to_hex(hex(offset), f)
|
|
||||||
break
|
|
||||||
|
|
||||||
if hdr.name != RESOURCE_NODE:
|
|
||||||
raise PeFileError("no root resource node found")
|
|
||||||
|
|
||||||
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
|
|
||||||
total = table.number_of_name_entries + table.number_of_id_entries
|
|
||||||
|
|
||||||
for entry in range(total):
|
|
||||||
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
|
|
||||||
if entry.name_or_id == VERSION_RESOURCE:
|
|
||||||
while entry.data_or_subdir & (1 << 31):
|
|
||||||
shift = entry.data_or_subdir & ~(1 << 31)
|
|
||||||
seek_to_hex(hex(offset + shift), f)
|
|
||||||
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
|
|
||||||
total = (
|
|
||||||
table.number_of_name_entries
|
|
||||||
+ table.number_of_id_entries
|
|
||||||
)
|
|
||||||
for entry in range(total):
|
|
||||||
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
|
|
||||||
break
|
|
||||||
if entry.name_or_id > VERSION_RESOURCE:
|
|
||||||
raise PeFileError("no version info node found")
|
|
||||||
seek_to_hex(hex(offset + entry.data_or_subdir), f)
|
|
||||||
|
|
||||||
data = RESOURCE_DATA_ENTRY.unpack(f)
|
|
||||||
# https://stackoverflow.com/questions/2170843/va-virtual-address-rva-relative-virtual-address
|
|
||||||
offset = data.data_rva - hdr.virtual_address + hdr.pointer_to_raw_data
|
|
||||||
seek_to_hex(hex(offset), f)
|
|
||||||
|
|
||||||
hdr = VS_VERSION_INFO_HDR.unpack(f)
|
|
||||||
# https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
|
|
||||||
byte_len = len(VS_VERSION_INFO_ID.encode("utf-16le"))
|
|
||||||
label = f.read(byte_len).decode("utf-16le")
|
|
||||||
if label != VS_VERSION_INFO_ID:
|
|
||||||
raise PeFileError(f"header identifier != '{VS_VERSION_INFO_ID}'")
|
|
||||||
f.read(32 - byte_len)
|
|
||||||
|
|
||||||
identifier = hex(struct.unpack("<Q", f.read(8))[0])
|
|
||||||
if identifier != VS_VERSION_INFO_MAGIC:
|
|
||||||
raise PeFileError(
|
|
||||||
f"{VS_VERSION_INFO_ID} address != '{VS_VERSION_INFO_MAGIC}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
version = parse_version_number(f)
|
|
||||||
except Exception as e:
|
|
||||||
return PeFileError(e)
|
|
||||||
|
|
||||||
return version
|
|
||||||
|
|
||||||
|
|
||||||
def get_pefile_path(path: str, appid: int) -> Path:
|
|
||||||
binary = "DayZ_x64.exe"
|
|
||||||
identifier = {221100: "DayZ", 1024020: "DayZ Exp"}
|
|
||||||
name = identifier[appid]
|
|
||||||
|
|
||||||
pe_path = None
|
|
||||||
path = path + "/steamapps/libraryfolders.vdf"
|
|
||||||
|
|
||||||
with open(path, "r") as f:
|
|
||||||
try:
|
|
||||||
j = json.loads(vdf_to_json(f))
|
|
||||||
except Exception:
|
|
||||||
raise VDFLoadError("Failed to parse libraryfolders")
|
|
||||||
|
|
||||||
for obj in j["libraryfolders"]:
|
|
||||||
if str(appid) in j["libraryfolders"][obj]["apps"]:
|
|
||||||
pe_path = j["libraryfolders"][obj]["path"]
|
|
||||||
pe_path += f"/steamapps/common/{name}/{binary}"
|
|
||||||
break
|
|
||||||
|
|
||||||
if pe_path is None:
|
|
||||||
raise AppNotInstalledError(
|
|
||||||
f"Failed to find a libraryfolder for the appid '{appid}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
pe_path = Path(pe_path)
|
|
||||||
if pe_path.exists() is False:
|
|
||||||
raise AppMovedError(
|
|
||||||
f"Path '{pe_path}' specified in libraryfolders does not exist"
|
|
||||||
)
|
|
||||||
|
|
||||||
return pe_path
|
|
||||||
|
|
||||||
|
|
||||||
def compare_versions(local: DayZVersion, remote: DayZVersion):
|
|
||||||
"""
|
|
||||||
packaging.version module is not available OOTB on some distributions
|
|
||||||
"""
|
|
||||||
if dayz_version_to_str(local) == dayz_version_to_str(remote):
|
|
||||||
return VersionMatch.SAME_VERSION
|
|
||||||
|
|
||||||
if local.major < remote.major:
|
|
||||||
return VersionMatch.LOCAL_OLDER
|
|
||||||
if local.major > remote.major:
|
|
||||||
return VersionMatch.LOCAL_NEWER
|
|
||||||
if local.major == remote.major:
|
|
||||||
if local.minor < remote.minor:
|
|
||||||
return VersionMatch.LOCAL_OLDER
|
|
||||||
if local.minor > remote.minor:
|
|
||||||
return VersionMatch.LOCAL_NEWER
|
|
||||||
if local.minor == remote.minor:
|
|
||||||
if local.patch < remote.patch:
|
|
||||||
return VersionMatch.LOCAL_OLDER
|
|
||||||
if local.patch > remote.patch:
|
|
||||||
return VersionMatch.LOCAL_NEWER
|
|
||||||
if local.patch == remote.patch:
|
|
||||||
return VersionMatch.SAME_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
def vdf_to_json(stream):
|
|
||||||
def _istr(indent, string):
|
|
||||||
return (indent * " ") + string
|
|
||||||
|
|
||||||
jbuf = "{\n"
|
|
||||||
lex = shlex(stream)
|
|
||||||
indent = 1
|
|
||||||
|
|
||||||
while True:
|
|
||||||
tok = lex.get_token()
|
|
||||||
if not tok:
|
|
||||||
return jbuf + "}\n"
|
|
||||||
if tok == "}":
|
|
||||||
indent -= 1
|
|
||||||
jbuf += _istr(indent, "}")
|
|
||||||
ntok = lex.get_token()
|
|
||||||
lex.push_token(ntok)
|
|
||||||
if ntok and ntok != "}":
|
|
||||||
jbuf += ","
|
|
||||||
jbuf += "\n"
|
|
||||||
else:
|
|
||||||
ntok = lex.get_token()
|
|
||||||
if ntok == "{":
|
|
||||||
jbuf += _istr(indent, tok + ": {\n")
|
|
||||||
indent += 1
|
|
||||||
else:
|
|
||||||
jbuf += _istr(indent, tok + ": " + ntok)
|
|
||||||
ntok = lex.get_token()
|
|
||||||
lex.push_token(ntok)
|
|
||||||
if ntok != "}":
|
|
||||||
jbuf += ","
|
|
||||||
jbuf += "\n"
|
|
||||||
@ -1,472 +0,0 @@
|
|||||||
import json
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import typing # noqa
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from urllib import request, parse
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
sys.path.append("a2s")
|
|
||||||
import a2s # noqa
|
|
||||||
from a2s import dayzquery # noqa
|
|
||||||
|
|
||||||
params = [
|
|
||||||
r"\nor\1\map\chernarusplus\nor\1\map\sakhal\nor\1\map\enoch\empty\1\nor\1\map\namalsk", # noqa
|
|
||||||
r"\map\namalsk\empty\1",
|
|
||||||
r"\map\namalsk\noplayers\1",
|
|
||||||
r"\map\chernarusplus\empty\1",
|
|
||||||
r"\map\chernarusplus\noplayers\1",
|
|
||||||
r"\map\sakhal\empty\1",
|
|
||||||
r"\map\sakhal\noplayers\1",
|
|
||||||
r"\map\enoch\empty\1",
|
|
||||||
r"\map\enoch\noplayers\1",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BmAPIError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BmIdError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidIpError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_netmask() -> str:
|
|
||||||
hostname = os.uname()[1]
|
|
||||||
i = socket.gethostbyname(hostname)
|
|
||||||
netmask = i.rsplit(".", 1)[0]
|
|
||||||
return netmask
|
|
||||||
|
|
||||||
|
|
||||||
def test_ip(suffix: int, port: int) -> dict | None:
|
|
||||||
netmask = get_netmask()
|
|
||||||
hostname = f"{netmask}.{str(suffix)}"
|
|
||||||
ping = ["ping", "-c1", "-i", "0.1", "-w", "1"]
|
|
||||||
output = subprocess.run(ping + [hostname], capture_output=True)
|
|
||||||
if output.returncode == 0:
|
|
||||||
return query_direct(hostname, port)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize(name: str) -> str:
|
|
||||||
name = re.sub(r"\r", r"", name)
|
|
||||||
name = re.sub(r"\n", r"", name)
|
|
||||||
name = re.sub(r"\x01", r"", name)
|
|
||||||
name = re.sub(r"\ufeff", r"", name)
|
|
||||||
name = re.sub(r"(^!\s*)", r"-", name)
|
|
||||||
name = re.sub(r"(^-\s*)", r"", name)
|
|
||||||
name = re.sub(r"(^-)", r"", name)
|
|
||||||
name = re.sub(r"(^\s*)", r"", name)
|
|
||||||
name = re.sub(r"\t", "", name)
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def parse_json(json: list) -> list:
|
|
||||||
"""
|
|
||||||
Server metadata is underspecified and server operators
|
|
||||||
tend to insert random garbage in the headers. In case
|
|
||||||
sanitization failed, discard malformed rows rather than
|
|
||||||
aborting outright.
|
|
||||||
"""
|
|
||||||
rows = []
|
|
||||||
for row in json:
|
|
||||||
try:
|
|
||||||
name = sanitize(row["name"])
|
|
||||||
if name == "":
|
|
||||||
continue
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
malformed = False
|
|
||||||
for key in [
|
|
||||||
"map",
|
|
||||||
"gametype",
|
|
||||||
"players",
|
|
||||||
"max_players",
|
|
||||||
"addr",
|
|
||||||
"gameport",
|
|
||||||
]:
|
|
||||||
try:
|
|
||||||
row[key]
|
|
||||||
except KeyError:
|
|
||||||
malformed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if malformed is True:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = row["gametype"].split(",")
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "no3rd" in r:
|
|
||||||
view = "1PP"
|
|
||||||
else:
|
|
||||||
view = "3PP"
|
|
||||||
|
|
||||||
if "external" in r:
|
|
||||||
provider = "Unoffic."
|
|
||||||
else:
|
|
||||||
provider = "Official"
|
|
||||||
|
|
||||||
if "mod" in r:
|
|
||||||
modded = True
|
|
||||||
else:
|
|
||||||
modded = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = row["gametype"].split("lqs")
|
|
||||||
queue = r[1].split(",")[0]
|
|
||||||
except IndexError:
|
|
||||||
queue = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
test_time = re.search(r"[0-9]{2}:[0-9]{2}", row["gametype"])
|
|
||||||
if test_time:
|
|
||||||
time = test_time.group(0)
|
|
||||||
else:
|
|
||||||
time = "Unknown"
|
|
||||||
except AttributeError:
|
|
||||||
time = "Unknown"
|
|
||||||
|
|
||||||
try:
|
|
||||||
ip = row["addr"].split(":")[0] + ":" + str(row["gameport"])
|
|
||||||
qport = row["addr"].split(":")[1]
|
|
||||||
except IndexError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
ping = row["ping"]
|
|
||||||
except KeyError:
|
|
||||||
ping = 9999
|
|
||||||
|
|
||||||
mapname = row["map"].lower()
|
|
||||||
players = row["players"]
|
|
||||||
max_players = row["max_players"]
|
|
||||||
raw = [
|
|
||||||
name,
|
|
||||||
mapname,
|
|
||||||
view,
|
|
||||||
time,
|
|
||||||
int(players),
|
|
||||||
int(max_players),
|
|
||||||
int(queue),
|
|
||||||
ip,
|
|
||||||
int(qport),
|
|
||||||
ping,
|
|
||||||
provider,
|
|
||||||
modded,
|
|
||||||
]
|
|
||||||
rows.append(raw)
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def query_direct(ip: str, qport: int, TIMEOUT: float=3.0) -> dict | None:
|
|
||||||
try:
|
|
||||||
info = a2s.info((ip, qport), TIMEOUT)
|
|
||||||
|
|
||||||
name = info.server_name
|
|
||||||
mapname = info.map_name
|
|
||||||
address = ip + ":" + str(qport)
|
|
||||||
gameport = str(info.port)
|
|
||||||
players = info.player_count
|
|
||||||
max_players = info.max_players
|
|
||||||
keywords = info.keywords
|
|
||||||
|
|
||||||
try:
|
|
||||||
ping = info.ping
|
|
||||||
ping = math.floor(info.ping * 1000)
|
|
||||||
except AttributeError:
|
|
||||||
ping = 9999
|
|
||||||
|
|
||||||
res = {}
|
|
||||||
res["name"] = name
|
|
||||||
res["map"] = mapname
|
|
||||||
res["gametype"] = keywords
|
|
||||||
res["players"] = players
|
|
||||||
res["max_players"] = max_players
|
|
||||||
res["addr"] = address
|
|
||||||
res["gameport"] = gameport
|
|
||||||
res["ping"] = ping
|
|
||||||
return res
|
|
||||||
except TimeoutError:
|
|
||||||
return None
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class Res:
|
|
||||||
status: int
|
|
||||||
parsed: bool
|
|
||||||
json: Union[str, None]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class Ping:
|
|
||||||
addr: str
|
|
||||||
iteration: int
|
|
||||||
ping: int
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class Details:
|
|
||||||
data: Union[list, None]
|
|
||||||
description: str
|
|
||||||
success: bool
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class Prereqs:
|
|
||||||
password: bool
|
|
||||||
gameport: int
|
|
||||||
appid: Union[int, None]
|
|
||||||
version: Union[str, None]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class Record:
|
|
||||||
"""
|
|
||||||
The gameport field is manipulated by the RowType.CONN_BY_IP method
|
|
||||||
"""
|
|
||||||
ip: str
|
|
||||||
gameport: int
|
|
||||||
qport: int
|
|
||||||
|
|
||||||
|
|
||||||
def get_prereqs(ip: str, qport: int) -> Prereqs:
|
|
||||||
try:
|
|
||||||
info = a2s.info((ip, qport))
|
|
||||||
except TimeoutError:
|
|
||||||
return Prereqs(False, 0, None, None)
|
|
||||||
|
|
||||||
gameport = getattr(info, "port", 0)
|
|
||||||
is_password = getattr(info, "password_protected", False)
|
|
||||||
appid = getattr(info, "game_id", None)
|
|
||||||
version = getattr(info, "version", None)
|
|
||||||
|
|
||||||
return Prereqs(is_password, gameport, appid, version)
|
|
||||||
|
|
||||||
|
|
||||||
def details(ip: str, qport: int) -> Details:
|
|
||||||
default_str = "None provided"
|
|
||||||
|
|
||||||
try:
|
|
||||||
info = a2s.info((ip, qport))
|
|
||||||
except TimeoutError:
|
|
||||||
return Details(None, default_str, False)
|
|
||||||
try:
|
|
||||||
rules = dayzquery.dayz_rules((ip, int(qport)))
|
|
||||||
except TimeoutError:
|
|
||||||
return Details(None, default_str, False)
|
|
||||||
|
|
||||||
try:
|
|
||||||
keywords = info.keywords.split(",")
|
|
||||||
except AttributeError:
|
|
||||||
return Details(None, default_str, False)
|
|
||||||
|
|
||||||
battleye = "Disabled"
|
|
||||||
if "battleye" in keywords:
|
|
||||||
battleye = "Enabled"
|
|
||||||
|
|
||||||
day_accel = 0.0
|
|
||||||
night_accel = 0.0
|
|
||||||
for keyword in keywords:
|
|
||||||
if "etm" in keyword:
|
|
||||||
day_accel = float(keyword.lstrip("etm"))
|
|
||||||
day_accel = float(f"{day_accel:g}")
|
|
||||||
if "entm" in keywords:
|
|
||||||
night_accel = float(keyword.lstrip("entm"))
|
|
||||||
night_accel = float(f"{night_accel:g}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
password = info.password_protected
|
|
||||||
if password is False:
|
|
||||||
password = "Disabled"
|
|
||||||
else:
|
|
||||||
password = "Enabled"
|
|
||||||
except AttributeError:
|
|
||||||
password = "-"
|
|
||||||
|
|
||||||
try:
|
|
||||||
vac = info.vac_enabled
|
|
||||||
if vac is False:
|
|
||||||
vac = "Disabled"
|
|
||||||
else:
|
|
||||||
vac = "Enabled"
|
|
||||||
except AttributeError:
|
|
||||||
vac = "-"
|
|
||||||
|
|
||||||
try:
|
|
||||||
version = info.version
|
|
||||||
except AttributeError:
|
|
||||||
version = "-"
|
|
||||||
|
|
||||||
try:
|
|
||||||
dlc = rules.dlc_flags
|
|
||||||
if dlc == 0:
|
|
||||||
dlc = "None"
|
|
||||||
if dlc == 2:
|
|
||||||
dlc = "Frostline"
|
|
||||||
except AttributeError:
|
|
||||||
dlc = "not specified"
|
|
||||||
|
|
||||||
try:
|
|
||||||
platform = rules.platform
|
|
||||||
if platform == "win":
|
|
||||||
platform = "Windows"
|
|
||||||
if platform == "?":
|
|
||||||
platform = "Linux"
|
|
||||||
except AttributeError:
|
|
||||||
platform = "not specified"
|
|
||||||
|
|
||||||
try:
|
|
||||||
description = rules.description.strip()
|
|
||||||
if description == "":
|
|
||||||
description = default_str
|
|
||||||
except AttributeError:
|
|
||||||
description = default_str
|
|
||||||
|
|
||||||
rows = [
|
|
||||||
["Battleye", battleye],
|
|
||||||
["Daytime acceleration", f"{day_accel}x"],
|
|
||||||
["DLC", dlc],
|
|
||||||
["Night-time acceleration", f"{night_accel}x"],
|
|
||||||
["Password", password],
|
|
||||||
["Platform", platform],
|
|
||||||
["Valve Anti-Cheat", vac],
|
|
||||||
["Version", version],
|
|
||||||
]
|
|
||||||
|
|
||||||
return Details(rows, description, True)
|
|
||||||
|
|
||||||
|
|
||||||
def ping(iteration: int, row: list) -> Ping:
|
|
||||||
addr = row[7]
|
|
||||||
qport = row[8]
|
|
||||||
|
|
||||||
res = None
|
|
||||||
|
|
||||||
if row[9] != 9999:
|
|
||||||
return Ping(addr, iteration, row[9])
|
|
||||||
|
|
||||||
try:
|
|
||||||
ip = addr.split(":")[0]
|
|
||||||
except IndexError:
|
|
||||||
ping = 9999
|
|
||||||
|
|
||||||
try:
|
|
||||||
res = query_direct(ip, qport, 0.5)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if res is None:
|
|
||||||
ping = 9999
|
|
||||||
else:
|
|
||||||
ping = res["ping"]
|
|
||||||
|
|
||||||
return Ping(addr, iteration, ping)
|
|
||||||
|
|
||||||
|
|
||||||
def query_api(key: str, appid: int, param: str) -> Res:
|
|
||||||
LIMIT = 10000
|
|
||||||
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
|
||||||
|
|
||||||
payload: dict[str, Union[int, str]] = {
|
|
||||||
"filter": r"\appid" + fr"\{appid}" + param,
|
|
||||||
"limit": LIMIT,
|
|
||||||
"key": key,
|
|
||||||
}
|
|
||||||
par = parse.urlencode(payload)
|
|
||||||
url = f"{url}{par}"
|
|
||||||
|
|
||||||
status = 200
|
|
||||||
parsed = True
|
|
||||||
data = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
with request.urlopen(url) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
status = response.status
|
|
||||||
try:
|
|
||||||
parsed = True
|
|
||||||
data = json.load(response)
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
parsed = False
|
|
||||||
data = None
|
|
||||||
except HTTPError:
|
|
||||||
status = 403
|
|
||||||
parsed = False
|
|
||||||
data = None
|
|
||||||
finally:
|
|
||||||
return Res(status, parsed, data)
|
|
||||||
|
|
||||||
|
|
||||||
def query_bm_api(api_key: str, bm_id: str) -> Record:
|
|
||||||
if bm_id.isnumeric() is False:
|
|
||||||
raise BmIdError("ID must be numeric only")
|
|
||||||
|
|
||||||
payload: dict[str, Union[int, str]] = {
|
|
||||||
"sort": "-players",
|
|
||||||
"filter[game]": "dayz",
|
|
||||||
"filter[ids][whitelist]": bm_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
url = "https://api.battlemetrics.com/servers?"
|
|
||||||
par = parse.urlencode(payload)
|
|
||||||
url = f"{url}{par}"
|
|
||||||
|
|
||||||
hdr = {"Authorization": "Bearer " + api_key}
|
|
||||||
r = request.Request(url, headers=hdr)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with request.urlopen(r) as response:
|
|
||||||
try:
|
|
||||||
j = json.load(response)
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
raise BmAPIError("Malformed response from Battlemetrics")
|
|
||||||
|
|
||||||
if len(j["data"]) < 1:
|
|
||||||
raise BmAPIError("Not a valid Battlemetrics ID")
|
|
||||||
j = j["data"][0]["attributes"]
|
|
||||||
return Record(j["ip"], j["port"], j["portQuery"])
|
|
||||||
except HTTPError:
|
|
||||||
raise BmAPIError("Failed to query Battlemetrics")
|
|
||||||
|
|
||||||
|
|
||||||
def validate_ip(addr: str) -> Record:
|
|
||||||
fields = addr.split(":")
|
|
||||||
if len(fields) != 2:
|
|
||||||
raise InvalidIpError("Address must be formatted as IP:Queryport")
|
|
||||||
|
|
||||||
ip = fields[0]
|
|
||||||
port = fields[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
int(port)
|
|
||||||
except ValueError:
|
|
||||||
raise InvalidIpError(f"'{port}' is not a valid port")
|
|
||||||
|
|
||||||
if int(port) > 65535 or int(port) < 0:
|
|
||||||
raise InvalidIpError(f"'{port}' is not a valid port")
|
|
||||||
|
|
||||||
try:
|
|
||||||
socket.inet_aton(ip)
|
|
||||||
except OSError:
|
|
||||||
raise InvalidIpError(f"'{ip}' is not a valid IP")
|
|
||||||
|
|
||||||
ip = addr.split(":")[0]
|
|
||||||
qport = int(addr.split(":")[1])
|
|
||||||
record = Record(ip, 0, qport)
|
|
||||||
return record
|
|
||||||
1
helpers/tools/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
latlon
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
bin := latlon
|
|
||||||
|
|
||||||
CC = gcc
|
|
||||||
|
|
||||||
CFLAGS += -Wall
|
|
||||||
CFLAGS += -Wextra
|
|
||||||
CFLAGS += -Wpedantic
|
|
||||||
CFLAGS += -std=c23
|
|
||||||
CFLAGS += -O3
|
|
||||||
|
|
||||||
LDFLAGS += -lm
|
|
||||||
|
|
||||||
PREFIX ?= $(HOME)/.local/share/dzgui
|
|
||||||
|
|
||||||
all: $(bin)
|
|
||||||
|
|
||||||
install: $(bin)
|
|
||||||
install -Dm 755 $(bin) $(PREFIX)
|
|
||||||
|
|
||||||
$(bin): latlon.c
|
|
||||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(bin)
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
// This small program computes the distance between two points on the Earths
|
|
||||||
// surface. The algorithm used for computing is called haversine formula.
|
|
||||||
// Source for the algorithm can be found here:
|
|
||||||
// https://en.wikipedia.org/wiki/Haversine_formula
|
|
||||||
|
|
||||||
#include <math.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
// Earth's radius in meters.
|
|
||||||
#define RADIUS ((12756L / 2.0L) * 1000L)
|
|
||||||
|
|
||||||
// PI natural constant
|
|
||||||
#define PI 3.1415926536L
|
|
||||||
|
|
||||||
// coordinates struct for storing points
|
|
||||||
struct Coordinate {
|
|
||||||
double latitude;
|
|
||||||
double longitude;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculates a radian value from an angle
|
|
||||||
*/
|
|
||||||
double radian_from(const double angle) {
|
|
||||||
return (angle * PI) / 180;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculate the haversine function from an angle in radian
|
|
||||||
*/
|
|
||||||
double haversine(const double angle) { return ((1.0L - cos(angle)) / 2); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculate the distance between two coordinates
|
|
||||||
*/
|
|
||||||
double great_circle_distance(const struct Coordinate a,
|
|
||||||
const struct Coordinate b) {
|
|
||||||
|
|
||||||
// calculate longitude and latitude differences
|
|
||||||
struct Coordinate diff_coord = {
|
|
||||||
.longitude = a.longitude - b.longitude,
|
|
||||||
.latitude = a.latitude - b.latitude,
|
|
||||||
};
|
|
||||||
|
|
||||||
// calculate haversine(theta) value, where theta is the angle between the
|
|
||||||
// two coordinates
|
|
||||||
double hav_theta = haversine(diff_coord.latitude)
|
|
||||||
+ cos(a.latitude) * cos(b.latitude) * haversine(diff_coord.longitude);
|
|
||||||
|
|
||||||
// calculate distance from radius, and haversine(theta) values
|
|
||||||
double distance = 2 * RADIUS * asin(sqrt(hav_theta));
|
|
||||||
|
|
||||||
return distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, const char* argv[]) {
|
|
||||||
if (argc < 5 || argc > 5) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const struct Coordinate a = {
|
|
||||||
.latitude = radian_from(atof(argv[1])),
|
|
||||||
.longitude = radian_from(atof(argv[2])),
|
|
||||||
};
|
|
||||||
const struct Coordinate b = {
|
|
||||||
.latitude = radian_from(atof(argv[3])),
|
|
||||||
.longitude = radian_from(atof(argv[4])),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the distance in meters.
|
|
||||||
// To pretty-print the result in a consumer of this module, it is
|
|
||||||
// recommended to divide the result by 1,000 and round it.
|
|
||||||
printf("%.1f\n", great_circle_distance(a, b));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
6534
helpers/ui.py
BIN
images/tutorial/01.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
images/tutorial/02.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
images/tutorial/03.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
images/tutorial/04.png
Normal file
|
After Width: | Height: | Size: 608 KiB |
BIN
images/tutorial/05.png
Normal file
|
After Width: | Height: | Size: 569 KiB |
BIN
images/tutorial/06.png
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
images/tutorial/07.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
images/tutorial/08.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
images/tutorial/09.png
Normal file
|
After Width: | Height: | Size: 31 KiB |