mirror of
https://github.com/aclist/dztui.git
synced 2026-08-25 17:32:36 +02:00
feat: parse pefile
This commit is contained in:
parent
8ffb1c7ea6
commit
ee73c2cfa3
123
helpers/funcs
123
helpers/funcs
@ -4,6 +4,7 @@ version="6.0.0-beta.5"
|
||||
|
||||
#CONSTANTS
|
||||
aid=221100
|
||||
exp=1024020
|
||||
game="dayz"
|
||||
app_name="dzgui"
|
||||
app_name_upper="DZGUI"
|
||||
@ -46,6 +47,7 @@ _cache_my_servers="$cache_dir/$prefix.my_servers"
|
||||
_cache_history="$cache_dir/$prefix.history"
|
||||
_cache_launch="$cache_dir/$prefix.launch_mods"
|
||||
_cache_address="$cache_dir/$prefix.launch_address"
|
||||
_cache_binary="$cache_dir/$prefix.binary"
|
||||
_cache_coords="$cache_path/$prefix.coords"
|
||||
_cache_cooldown="$cache_path/$prefix.cooldown"
|
||||
_cache_lan="$cache_path/$prefix.lan"
|
||||
@ -97,12 +99,9 @@ declare -A funcs=(
|
||||
["Change Steam API key"]="update_config_val"
|
||||
["Change Battlemetrics API key"]="update_config_val"
|
||||
["Change favorite server"]="add_record"
|
||||
["Quick-connect to favorite server"]="quick_connect"
|
||||
["Add server by IP"]="add_record"
|
||||
["Add server by ID"]="add_record"
|
||||
["Connect by IP"]="validate_and_connect"
|
||||
["Connect by ID"]="validate_and_connect"
|
||||
["Connect from table"]="connect_from_table"
|
||||
["try_connect"]="try_connect"
|
||||
["find_id"]="find_id"
|
||||
["toggle"]="toggle"
|
||||
["Open link"]="open_link"
|
||||
@ -126,11 +125,26 @@ declare -A funcs=(
|
||||
["Remove from history"]="remove_from_history"
|
||||
["Force update local mods"]="force_update"
|
||||
["Resolve IP"]="resolve_ip"
|
||||
["Handshake"]="final_handshake"
|
||||
["Handshake"]="handshake"
|
||||
["Handshake_EXP"]="handshake_exp"
|
||||
["get_player_count"]="get_player_count"
|
||||
["update_symlinks"]="update_symlinks"
|
||||
)
|
||||
|
||||
clone_symlinks(){
|
||||
local path="$(< $_cache_binary)"
|
||||
path=$(dirname "$path")
|
||||
for dir in $(find $game_dir -type l); do
|
||||
local link=$(basename $dir)
|
||||
ln -sf "${dir}" "${path}/${link}"
|
||||
done
|
||||
}
|
||||
handshake(){
|
||||
final_handshake "$aid"
|
||||
}
|
||||
handshake_exp(){
|
||||
final_handshake "$exp"
|
||||
}
|
||||
query_favorites(){
|
||||
if [[ -z "${ip_list[@]}" ]]; then
|
||||
return 1
|
||||
@ -191,44 +205,6 @@ get_player_count(){
|
||||
printf "%s\n%s" "$players" "$queue"
|
||||
}
|
||||
|
||||
validate_and_connect(){
|
||||
local context="$1"
|
||||
local addr="$2"
|
||||
|
||||
local record
|
||||
case "$context" in
|
||||
"Connect by ID")
|
||||
if [[ -z "$api_key" ]]; then
|
||||
printf "No Battlemetrics API key set"
|
||||
return 4
|
||||
fi
|
||||
record=$(map_id_to_ip "$addr")
|
||||
if [[ $? -eq 1 ]]; then
|
||||
logger WARN "Not a valid record: '$addr'"
|
||||
printf "Not a valid ID"
|
||||
return 2
|
||||
fi
|
||||
logger INFO "Battlemetrics ID resolved to IP $record"
|
||||
;;
|
||||
"Connect by IP")
|
||||
if [[ $(validate_ip "$addr") -eq 1 ]]; then
|
||||
printf "Not a valid IP format. Supply IP:Queryport"
|
||||
return 2
|
||||
fi
|
||||
local ip=$(<<< $addr awk -F: '{print $1}')
|
||||
local qport=$(<<< $addr awk -F: '{print $2}')
|
||||
local res
|
||||
res=$(a2s $ip $qport info)
|
||||
if [[ ! $? -eq 0 ]]; then
|
||||
printf "Timed out when querying the server. Is this a valid server?"
|
||||
return 2
|
||||
fi
|
||||
local gameport="$(<<< $res jq -r '.[].gameport')"
|
||||
record="${ip}:${gameport}:${qport}"
|
||||
logger INFO "Record resolved to $record"
|
||||
esac
|
||||
try_connect "$record"
|
||||
}
|
||||
map_id_to_ip(){
|
||||
local id="$1"
|
||||
local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
|
||||
@ -294,21 +270,6 @@ add_record(){
|
||||
;;
|
||||
esac
|
||||
}
|
||||
connect_by_id(){
|
||||
if [[ $(validate_ip "$addr") -eq 1 ]]; then
|
||||
printf "Not a valid IP format. Supply IP:Queryport"
|
||||
return 2
|
||||
fi
|
||||
local ip=$(<<< $addr awk -F: '{print $1}')
|
||||
local qport=$(<<< $addr awk -F: '{print $2}')
|
||||
local res
|
||||
res=$(a2s $ip $qport info)
|
||||
if [[ ! $? -eq 0 ]]; then
|
||||
printf "Timed out when querying the server. Is this a valid server?"
|
||||
return 2
|
||||
fi
|
||||
#res contains modlist
|
||||
}
|
||||
start_cooldown(){
|
||||
logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
|
||||
date +%s > $_cache_cooldown
|
||||
@ -444,6 +405,7 @@ query_config(){
|
||||
"fav_label"
|
||||
"preferred_client"
|
||||
"fullscreen"
|
||||
"default_steam_path"
|
||||
)
|
||||
if [[ -n $key ]]; then
|
||||
if [[ -n ${!key} ]]; then
|
||||
@ -924,19 +886,6 @@ open_link(){
|
||||
xdg-open "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
quick_connect(){
|
||||
if [[ -z $fav_server ]]; then
|
||||
printf "No favorite server currently set"
|
||||
return 1
|
||||
fi
|
||||
try_connect "$fav_server"
|
||||
}
|
||||
connect_from_table(){
|
||||
shift
|
||||
local record="$1"
|
||||
try_connect "$record"
|
||||
}
|
||||
generate_log(){
|
||||
source $config_file
|
||||
cat <<-DOC > $system_log
|
||||
@ -1126,10 +1075,17 @@ try_fallback(){
|
||||
esac
|
||||
}
|
||||
try_connect(){
|
||||
shift
|
||||
local record="$1"
|
||||
local appid="$2"
|
||||
local binary="$3"
|
||||
|
||||
local ip=$(<<< $record awk -F: '{print $1}')
|
||||
local gameport=$(<<< $record awk -F: '{print $2}')
|
||||
local qport=$(<<< $record awk -F: '{print $3}')
|
||||
|
||||
echo "$binary" > $_cache_binary
|
||||
|
||||
local remote_mods
|
||||
remote_mods=$(a2s $ip $qport rules)
|
||||
if [[ $? -eq 1 ]]; then
|
||||
@ -1154,11 +1110,11 @@ try_connect(){
|
||||
return 1
|
||||
fi
|
||||
case $auto_install in
|
||||
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods";;
|
||||
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" ;;
|
||||
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "$appid";;
|
||||
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" "$appid" ;;
|
||||
esac
|
||||
else
|
||||
launch "$ip" "$gameport" "$sanitized_mods"
|
||||
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
||||
fi
|
||||
}
|
||||
check_architecture(){
|
||||
@ -1176,7 +1132,7 @@ force_update(){
|
||||
fi
|
||||
rm "$versions_file"
|
||||
local update=$(check_timestamps)
|
||||
manual_mod_install "null" "null" "$update" "null" "force"
|
||||
manual_mod_install "null" "null" "$update" "null" "force" "null"
|
||||
echo "Finished requesting mod updates."
|
||||
return 0
|
||||
}
|
||||
@ -1293,6 +1249,8 @@ launch(){
|
||||
local ip="$1"
|
||||
local gameport="$2"
|
||||
local mods="$3"
|
||||
local appid="$4"
|
||||
|
||||
local concat
|
||||
if [[ -n $mods ]]; then
|
||||
concat=$(concat_mods "$mods")
|
||||
@ -1301,18 +1259,22 @@ launch(){
|
||||
fi
|
||||
|
||||
update_symlinks
|
||||
[[ $appid -eq $exp ]] && clone_symlinks
|
||||
|
||||
if [[ $debug -eq 1 ]]; then
|
||||
local launch_options="$steam_cmd -applaunch $aid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
|
||||
local launch_options="$steam_cmd -applaunch $appid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
|
||||
printf "Debug mode: these options would have been used to launch the game: $launch_options"
|
||||
return 0
|
||||
fi
|
||||
echo "$concat" > "$_cache_launch"
|
||||
echo "$ip:$gameport" > "$_cache_address"
|
||||
logger INFO "Saved launch params: '$concat'"
|
||||
printf "Launch conditions satisfied. DayZ will now launch after you confirm this dialog."
|
||||
return 100
|
||||
printf "Launch conditions satisfied. DayZ will launch after you confirm this dialog."
|
||||
[[ $appid == "$aid" ]] && return 100
|
||||
[[ $appid == "$exp" ]] && return 101
|
||||
}
|
||||
final_handshake(){
|
||||
local appid="$1"
|
||||
local saved_mods=$(< "$_cache_launch")
|
||||
local saved_address=$(< "$_cache_address")
|
||||
local res=$(is_dayz_running)
|
||||
@ -1329,7 +1291,7 @@ final_handshake(){
|
||||
params+=("-skipintro")
|
||||
params+=("-name=$name")
|
||||
params+=("-mod=$saved_mods")
|
||||
$steam_cmd -applaunch $aid "${params[@]}" &
|
||||
$steam_cmd -applaunch $appid "${params[@]}" &
|
||||
until [[ $(is_dayz_running) -eq 1 ]]; do
|
||||
sleep 0.1s
|
||||
done
|
||||
@ -1343,6 +1305,7 @@ manual_mod_install(){
|
||||
local diff="$3"
|
||||
local sanitized_mods="$4"
|
||||
local mode="$5"
|
||||
local appid="$6"
|
||||
local ex="$state_path/dzg.watcher"
|
||||
|
||||
readarray -t stage_mods <<< "$diff"
|
||||
@ -1400,7 +1363,7 @@ manual_mod_install(){
|
||||
rm "$versions_file"
|
||||
check_timestamps
|
||||
fi
|
||||
launch "$ip" "$gameport" "$sanitized_mods"
|
||||
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
||||
else
|
||||
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
|
||||
exit 1
|
||||
|
||||
499
helpers/pefile.py
Normal file
499
helpers/pefile.py
Normal file
@ -0,0 +1,499 @@
|
||||
import json
|
||||
import struct
|
||||
import typing # noqa
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from packaging.version import Version
|
||||
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
|
||||
FAIL = 4
|
||||
|
||||
|
||||
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:
|
||||
version = get_version(file)
|
||||
patch = str(version.build) + str(version.revision)
|
||||
dz_vers = DayZVersion(version.major, version.minor, int(patch))
|
||||
return dz_vers
|
||||
|
||||
|
||||
def get_dayz_version_str(file: Path) -> str:
|
||||
v = get_dayz_version(file)
|
||||
concat = ".".join(str(el) for el in [v.major, v.minor, v.patch])
|
||||
return concat
|
||||
|
||||
|
||||
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 is_older_version(local: str, remote: str) -> bool:
|
||||
return Version(local) < Version(remote)
|
||||
|
||||
|
||||
def is_newer_version(local: str, remote: str) -> bool:
|
||||
return Version(local) > Version(remote)
|
||||
|
||||
|
||||
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(remote: str, appid: int, path: str):
|
||||
if appid == 221100:
|
||||
build = "DayZ"
|
||||
else:
|
||||
build = "DayZ Experimental"
|
||||
|
||||
local = None
|
||||
pe_filepath = None
|
||||
error = None
|
||||
|
||||
try:
|
||||
pe_filepath = get_pefile_path(path, appid)
|
||||
except Exception as e:
|
||||
return Result(
|
||||
local, remote, build, pe_filepath, VersionMatch.FAIL, e
|
||||
)
|
||||
|
||||
try:
|
||||
local = get_dayz_version_str(pe_filepath)
|
||||
except PeFileError:
|
||||
return Result(
|
||||
local, remote, build, pe_filepath, VersionMatch.FAIL, error
|
||||
)
|
||||
|
||||
if is_older_version(local, remote):
|
||||
res = VersionMatch.LOCAL_OLDER
|
||||
elif is_newer_version(local, remote):
|
||||
res = VersionMatch.LOCAL_NEWER
|
||||
else:
|
||||
res = VersionMatch.SAME_VERSION
|
||||
|
||||
return Result(local, remote, build, pe_filepath, res, error)
|
||||
|
||||
|
||||
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'
|
||||
@ -29,6 +29,18 @@ params = [
|
||||
]
|
||||
|
||||
|
||||
class BmAPIError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BmIdError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidIpError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_netmask() -> str:
|
||||
hostname = os.uname()[1]
|
||||
i = socket.gethostbyname(hostname)
|
||||
@ -196,7 +208,7 @@ class Res:
|
||||
json: Union[str, None]
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Ping:
|
||||
addr: str
|
||||
iteration: int
|
||||
@ -210,17 +222,33 @@ class Details:
|
||||
success: bool
|
||||
|
||||
|
||||
def is_passworded(ip: str, qport: int) -> bool:
|
||||
@dataclass
|
||||
class Prereqs:
|
||||
password: bool
|
||||
gameport: int
|
||||
appid: Union[int, None]
|
||||
version: Union[str, None]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Record:
|
||||
ip: str
|
||||
gameport: int
|
||||
qport: int
|
||||
|
||||
|
||||
def get_prereqs(ip: str, qport: int) -> Prereqs:
|
||||
try:
|
||||
info = a2s.info((ip, qport))
|
||||
except TimeoutError:
|
||||
return False
|
||||
return Prereqs(False, 0, None, None)
|
||||
|
||||
try:
|
||||
password = info.password_protected
|
||||
except AttributeError:
|
||||
return False
|
||||
return password
|
||||
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:
|
||||
@ -343,12 +371,12 @@ def ping(iteration: int, row: list) -> Ping:
|
||||
return Ping(addr, iteration, ping)
|
||||
|
||||
|
||||
def query_api(key: str, param: str) -> Res:
|
||||
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\221100" + param,
|
||||
"filter": r"\appid" + fr"\{appid}" + param,
|
||||
"limit": LIMIT,
|
||||
"key": key,
|
||||
}
|
||||
@ -375,3 +403,62 @@ def query_api(key: str, param: str) -> Res:
|
||||
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):
|
||||
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
|
||||
|
||||
347
helpers/ui.py
347
helpers/ui.py
@ -13,21 +13,24 @@ import threading
|
||||
import typing # noqa
|
||||
import warnings
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import wait
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Literal, Self, Any
|
||||
|
||||
import servers as Servers # noqa E402
|
||||
import pefile as PeFile # noqa E402
|
||||
|
||||
from pefile import VDFLoadError, AppNotInstalledError, AppMovedError, PeFileError
|
||||
from pefile import VersionMatch
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
|
||||
import gi # noqa E402
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa: E402
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
|
||||
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
|
||||
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
||||
@ -37,6 +40,9 @@ app_name_lower = app_name.lower()
|
||||
app_name_abbr = "dzg"
|
||||
delimiter = "␞"
|
||||
|
||||
APPID_DAYZ = 221100
|
||||
APPID_DAYZ_EXP = 1024020
|
||||
|
||||
cache: dict[str, int] = {}
|
||||
config_vals: list[str] = []
|
||||
|
||||
@ -87,6 +93,13 @@ If this issue persists, your API key may be defunct.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Record:
|
||||
ip: str
|
||||
gameport: int
|
||||
qport: int
|
||||
|
||||
|
||||
class Preferences(Enum):
|
||||
STEAM = 1
|
||||
BM = 2
|
||||
@ -349,6 +362,12 @@ class RowType(EnumWithAttrs):
|
||||
"wait_msg": "Waiting for DayZ",
|
||||
"type": Command.ONESHOT,
|
||||
}
|
||||
HANDSHAKE_EXP = {
|
||||
"label": "Handshake_EXP",
|
||||
"tooltip": None,
|
||||
"wait_msg": "Waiting for DayZ",
|
||||
"type": Command.ONESHOT,
|
||||
}
|
||||
DELETE_SELECTED = {
|
||||
"label": "Delete selected mods",
|
||||
"tooltip": None,
|
||||
@ -666,6 +685,7 @@ def save_res_and_quit(*args) -> None:
|
||||
def suppress_signal(
|
||||
owner: Gtk.Widget, widget: Gtk.Widget, func_name: str, state: bool
|
||||
) -> None:
|
||||
|
||||
func = getattr(owner, func_name)
|
||||
if state:
|
||||
logger.debug(f"Blocking {func_name} for {widget}")
|
||||
@ -705,6 +725,7 @@ def format_metadata(row_sel: str) -> str:
|
||||
"fav_label": config_vals[4],
|
||||
"preferred_client": config_vals[5],
|
||||
"fullscreen": config_vals[6],
|
||||
"default_steam_path": config_vals[7]
|
||||
}
|
||||
if row is None:
|
||||
return ""
|
||||
@ -899,6 +920,11 @@ def process_shell_return_code(
|
||||
if final_conf == 1 or final_conf is None:
|
||||
return
|
||||
process_tree_option(RowType.HANDSHAKE)
|
||||
case 101: # final handshake, exp
|
||||
final_conf = spawn_dialog(msg, Popup.CONFIRM)
|
||||
if final_conf == 1 or final_conf is None:
|
||||
return
|
||||
process_tree_option(RowType.HANDSHAKE_EXP)
|
||||
case 255: # dzgui version update
|
||||
msg = "Update complete. Please close DZGUI and restart."
|
||||
spawn_dialog(msg, Popup.QUIT)
|
||||
@ -973,6 +999,22 @@ def process_tree_option(choice: RowType) -> None:
|
||||
App.grid.notebook.set_page_by_enum(NotebookPage.CHANGELOG)
|
||||
return
|
||||
|
||||
if command == RowType.QUICK_CONNECT:
|
||||
record = query_config("fav_server")[0]
|
||||
if record == "":
|
||||
spawn_dialog("No favorite server currently set", Popup.NOTIFY)
|
||||
return
|
||||
|
||||
record = str_to_record(record)
|
||||
thread_new_with_dialog(
|
||||
App.treeview.prepare_connection,
|
||||
parse_shell_output,
|
||||
"Querying server",
|
||||
command,
|
||||
[record]
|
||||
)
|
||||
return
|
||||
|
||||
match command.dict["type"]:
|
||||
case Command.HELP:
|
||||
call_bash_func("Open link", cmd_string)
|
||||
@ -989,6 +1031,61 @@ def process_tree_option(choice: RowType) -> None:
|
||||
return
|
||||
|
||||
|
||||
def parse_shell_output(proc: subprocess.CompletedProcess, row: RowType):
|
||||
out = proc.stdout.splitlines()
|
||||
try:
|
||||
msg = out[-1]
|
||||
except IndexError:
|
||||
msg = ""
|
||||
process_shell_return_code(msg, proc.returncode, row)
|
||||
|
||||
|
||||
def thread_new_with_dialog(
|
||||
func: Callable,
|
||||
callback: Callable | None,
|
||||
msg: str,
|
||||
row: RowType | None,
|
||||
args: list
|
||||
) -> None:
|
||||
|
||||
"""
|
||||
Pop a GenericDialog transient to App.treeview and
|
||||
call a function on a thread, with optional callback.
|
||||
Chiefly used for connection-related subprocesses.
|
||||
|
||||
After completion, the dialog is destroyed in the main event loop
|
||||
and additional exception handling occurs.
|
||||
|
||||
This is intended as a bridge between legacy shell methods and the UI.
|
||||
A more abstracted version of call_on_thread() for when extra threaded
|
||||
processing occurs before calls to shell subprocesses.
|
||||
"""
|
||||
|
||||
def background(*args):
|
||||
def cleanup():
|
||||
App.treeview.dialog_hide()
|
||||
if exception is not None:
|
||||
spawn_dialog(str(exception), Popup.NOTIFY)
|
||||
process_user_input(row)
|
||||
return
|
||||
if callback is not None and proc is not None:
|
||||
callback(proc, row)
|
||||
|
||||
exception = None
|
||||
proc = None
|
||||
try:
|
||||
proc = func(*args)
|
||||
except Exception as e:
|
||||
exception = e
|
||||
GLib.idle_add(cleanup)
|
||||
return
|
||||
GLib.idle_add(cleanup)
|
||||
|
||||
App.treeview.dialog_show(msg)
|
||||
thread = threading.Thread(target=background, args=(args))
|
||||
thread.start()
|
||||
|
||||
|
||||
def process_toggle(command: RowType) -> None:
|
||||
cmd_string = command.dict["label"]
|
||||
match command:
|
||||
@ -1013,11 +1110,60 @@ def process_toggle(command: RowType) -> None:
|
||||
proc = call_out("toggle", cmd_string)
|
||||
|
||||
|
||||
def str_to_record(record: str) -> Record | None:
|
||||
r = record.split(":")
|
||||
if len(r) != 3:
|
||||
return None
|
||||
return Record(r[0], int(r[1]), int(r[2]))
|
||||
|
||||
|
||||
def record_to_str(record: Record) -> str:
|
||||
return f"{record.ip}:{record.gameport}:{record.qport}"
|
||||
|
||||
|
||||
def connect_by_ip(enum: RowType, response: str) -> None:
|
||||
def _prep(response: str) -> None:
|
||||
record = Servers.validate_ip(response)
|
||||
proc = App.treeview.prepare_connection(record)
|
||||
return proc
|
||||
|
||||
thread_new_with_dialog(
|
||||
_prep,
|
||||
parse_shell_output,
|
||||
"Querying IP",
|
||||
enum,
|
||||
[response]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def connect_by_id(enum: RowType, response: str, key: str) -> None:
|
||||
def _prep(key: str, response: str) -> None:
|
||||
record = Servers.query_bm_api(key, response)
|
||||
proc = App.treeview.prepare_connection(record)
|
||||
return proc
|
||||
|
||||
thread_new_with_dialog(
|
||||
_prep,
|
||||
parse_shell_output,
|
||||
"Querying API",
|
||||
enum,
|
||||
[key, response]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def process_user_input(enum: RowType) -> None:
|
||||
prompt = enum.dict["prompt"]
|
||||
link_label = enum.dict["link_label"]
|
||||
cmd_string = enum.dict["label"]
|
||||
|
||||
if enum == RowType.CONN_BY_ID:
|
||||
key = query_config("api_key")[0]
|
||||
if len(key) == 0:
|
||||
spawn_dialog("No Battlemetrics API key is set; see Options", Popup.NOTIFY)
|
||||
return
|
||||
|
||||
user_entry = EntryDialog(prompt, Popup.ENTRY, link_label)
|
||||
response = user_entry.get_input()
|
||||
|
||||
@ -1026,6 +1172,14 @@ def process_user_input(enum: RowType) -> None:
|
||||
return
|
||||
logger.info(f"User entered: '{response}'")
|
||||
|
||||
if enum == RowType.CONN_BY_IP:
|
||||
connect_by_ip(enum, response)
|
||||
return
|
||||
|
||||
if enum == RowType.CONN_BY_ID:
|
||||
connect_by_id(enum, response, key)
|
||||
return
|
||||
|
||||
show_wait_dialog = True
|
||||
wait_msg = "Working"
|
||||
call_on_thread(
|
||||
@ -1789,7 +1943,7 @@ class TreeView(Gtk.TreeView):
|
||||
it = self.get_current_iter()
|
||||
name = model.get_value(it, 0)
|
||||
record = self.get_record_dict()
|
||||
DetailsDialog(name, record["ip"], record["qport"])
|
||||
DetailsDialog(name, record.ip, record.qport)
|
||||
|
||||
def show_mods(self) -> None:
|
||||
record = self.get_record_string()
|
||||
@ -1976,7 +2130,7 @@ class TreeView(Gtk.TreeView):
|
||||
if not record:
|
||||
grid.statusbar.update_server_meta()
|
||||
return
|
||||
ip = record["ip"]
|
||||
ip = record.ip
|
||||
if ip in cache:
|
||||
km = cache[ip]
|
||||
grid.statusbar.append_distance(km)
|
||||
@ -2135,8 +2289,8 @@ class TreeView(Gtk.TreeView):
|
||||
addr = model[path][7]
|
||||
qport = model[path][8]
|
||||
ip = addr.split(":")[0]
|
||||
qport = str(qport)
|
||||
return {"ip": ip, "qport": qport}
|
||||
gameport = int(addr.split(":")[1])
|
||||
return Record(ip, gameport, qport)
|
||||
|
||||
def update_players(self, players: int) -> None:
|
||||
model = self.get_model()
|
||||
@ -2183,9 +2337,7 @@ class TreeView(Gtk.TreeView):
|
||||
record = self.get_record_dict()
|
||||
if not record:
|
||||
return
|
||||
ip = record["ip"]
|
||||
qport = record["qport"]
|
||||
data = call_out("get_player_count", ip, qport)
|
||||
data = call_out("get_player_count", record.ip, str(record.qport))
|
||||
if data.returncode == 1:
|
||||
wait_dialog.destroy()
|
||||
return
|
||||
@ -2195,10 +2347,10 @@ class TreeView(Gtk.TreeView):
|
||||
key = query_config("steam_api")[0]
|
||||
job = Servers.query_api
|
||||
params = Servers.params
|
||||
serv = []
|
||||
with ThreadPoolExecutor() as executor:
|
||||
futures = [executor.submit(job, key, param) for param in params]
|
||||
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
|
||||
wait(futures)
|
||||
serv = []
|
||||
for future in futures:
|
||||
res = future.result()
|
||||
if res.status != 200 or not res.parsed:
|
||||
@ -2208,7 +2360,13 @@ class TreeView(Gtk.TreeView):
|
||||
return
|
||||
j = res.json
|
||||
serv += j["response"]["servers"]
|
||||
parsed = Servers.parse_json(serv)
|
||||
|
||||
res = Servers.query_api(key, APPID_DAYZ_EXP, "")
|
||||
if res.status == 200 and res.parsed is True:
|
||||
j = res.json
|
||||
serv += j["response"]["servers"]
|
||||
|
||||
parsed = Servers.parse_json(serv)
|
||||
return parsed
|
||||
|
||||
def _dump_lan(self, port: int) -> list | None:
|
||||
@ -2649,27 +2807,15 @@ class TreeView(Gtk.TreeView):
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def _background_connection(
|
||||
self, dialog: "GenericDialog", record: str
|
||||
) -> None:
|
||||
def load():
|
||||
dialog.destroy()
|
||||
out = proc.stdout.splitlines()
|
||||
msg = out[-1]
|
||||
process_shell_return_code(msg, proc.returncode, record)
|
||||
def dialog_hide(self) -> None:
|
||||
if hasattr(self, "wait_dialog"):
|
||||
self.wait_dialog.destroy()
|
||||
|
||||
proc = call_out("Connect from table", record)
|
||||
GLib.idle_add(load)
|
||||
|
||||
def _attempt_connection(self) -> None:
|
||||
record = self.get_record_string()
|
||||
msg = "Querying server and aligning mods"
|
||||
wait_dialog = GenericDialog(msg, Popup.WAIT)
|
||||
wait_dialog.show_all()
|
||||
thread = threading.Thread(
|
||||
target=self._background_connection, args=(wait_dialog, record)
|
||||
)
|
||||
thread.start()
|
||||
def dialog_show(self, msg: str) -> None:
|
||||
if hasattr(self, "wait_dialog"):
|
||||
self.wait_dialog.destroy()
|
||||
self.wait_dialog = GenericDialog(msg, Popup.WAIT)
|
||||
self.wait_dialog.show_all()
|
||||
|
||||
def is_row_to_server_context(self, view: RowType) -> bool:
|
||||
"""Row activation that jumps into a server table"""
|
||||
@ -2709,6 +2855,86 @@ class TreeView(Gtk.TreeView):
|
||||
def get_view(self):
|
||||
return self.view
|
||||
|
||||
def prepare_connection(self, record: Record) -> subprocess.CompletedProcess | None:
|
||||
"""
|
||||
Always called on a thread with a dialog on the transient parent window
|
||||
"""
|
||||
prereqs = Servers.get_prereqs(record.ip, record.qport)
|
||||
if prereqs.appid is None:
|
||||
msg = "Timed out when querying server, check IP or try again later"
|
||||
spawn_dialog(msg, Popup.NOTIFY)
|
||||
return None
|
||||
|
||||
if prereqs.version is not None:
|
||||
path = query_config("default_steam_path")[0]
|
||||
result = PeFile.compare_versions(prereqs.version, prereqs.appid, path)
|
||||
|
||||
if result.error is not None:
|
||||
logger.warning(result.error)
|
||||
|
||||
if result.match == VersionMatch.FAIL:
|
||||
if isinstance(result.error, VDFLoadError) or isinstance(result.error, PeFileError):
|
||||
# permissive; file exists, but could not determine version
|
||||
pass
|
||||
if isinstance(result.error, AppNotInstalledError):
|
||||
if prereqs.appid == 1024020:
|
||||
msg = (
|
||||
"This server is running DayZ Experimental, a beta build. "
|
||||
"You can install DayZ Experimental by searching for it in "
|
||||
"your Steam library."
|
||||
)
|
||||
spawn_dialog(msg, Popup.NOTIFY)
|
||||
return None
|
||||
if isinstance(result.error, AppMovedError):
|
||||
msg = (
|
||||
f"Steam is reporting that {result.build} is installed at a non-existent location. "
|
||||
f"If you recently installed {result.build} or moved it to a different drive, "
|
||||
"restart Steam to allow these changes to synchronize, then try again."
|
||||
)
|
||||
spawn_dialog(msg, Popup.NOTIFY)
|
||||
return None
|
||||
|
||||
if result.match == VersionMatch.LOCAL_OLDER:
|
||||
msg = (
|
||||
f"This server is running a newer build ({result.remote}) of {result.build} than "
|
||||
f"your local version ({result.local}). You may be unable to connect. Proceed anyway?"
|
||||
)
|
||||
res = spawn_dialog(msg, Popup.CONFIRM)
|
||||
if res is True:
|
||||
return None
|
||||
|
||||
if result.match == VersionMatch.LOCAL_NEWER:
|
||||
msg = (
|
||||
f"This server is running an out-of-date build ({result.remote}) of {result.build}. "
|
||||
"You may be unable to connect. Proceed anyway?"
|
||||
)
|
||||
res = spawn_dialog(msg, Popup.CONFIRM)
|
||||
if res is True:
|
||||
return None
|
||||
|
||||
if prereqs.password is True:
|
||||
msg = (
|
||||
"This server is password-protected and you will be "
|
||||
"prompted when connecting. Do you want to proceed?"
|
||||
)
|
||||
res = spawn_dialog(msg, Popup.CONFIRM)
|
||||
if res is True:
|
||||
return None
|
||||
|
||||
"""
|
||||
When using RowType.CONN_BY_IP, the gameport needs to be interpolated
|
||||
"""
|
||||
|
||||
record.gameport = prereqs.gameport
|
||||
addr = record_to_str(record)
|
||||
proc = call_out(
|
||||
"try_connect",
|
||||
addr,
|
||||
str(prereqs.appid),
|
||||
str(result.path)
|
||||
)
|
||||
return proc
|
||||
|
||||
@signal_emission
|
||||
@update_window_labels
|
||||
def _on_row_activated(
|
||||
@ -2777,15 +3003,13 @@ class TreeView(Gtk.TreeView):
|
||||
record = self.get_record_dict()
|
||||
if record is None:
|
||||
return
|
||||
if Servers.is_passworded(record["ip"], int(record["qport"])):
|
||||
msg = (
|
||||
"This server is password-protected and you will be "
|
||||
"prompted when connecting. Do you want to proceed?"
|
||||
)
|
||||
res = spawn_dialog(msg, Popup.CONFIRM)
|
||||
if res is True:
|
||||
return
|
||||
self._attempt_connection()
|
||||
thread_new_with_dialog(
|
||||
self.prepare_connection,
|
||||
parse_shell_output,
|
||||
"Querying server",
|
||||
None,
|
||||
[record]
|
||||
)
|
||||
case _: # any other non-server option from the main menu
|
||||
process_tree_option(output)
|
||||
|
||||
@ -3042,7 +3266,7 @@ class LanDialog(Gtk.MessageDialog):
|
||||
|
||||
|
||||
class DetailsDialog(GenericDialog):
|
||||
def __init__(self, server_name: str, ip: str, qport: str):
|
||||
def __init__(self, server_name: str, ip: str, qport: int):
|
||||
super().__init__(server_name, Popup.DETAILS)
|
||||
|
||||
dialog_box = self.get_content_area()
|
||||
@ -3050,7 +3274,7 @@ class DetailsDialog(GenericDialog):
|
||||
self.set_size_request(800, 700)
|
||||
|
||||
self.ip = ip.split(":")[0]
|
||||
self.qport = int(qport)
|
||||
self.qport = qport
|
||||
self.store = Gtk.ListStore(str, str, Pango.Weight)
|
||||
|
||||
self.view = Gtk.TreeView(
|
||||
@ -3203,12 +3427,10 @@ class ModDialog(GenericDialog):
|
||||
self.run()
|
||||
self.destroy()
|
||||
|
||||
addr = App.treeview.get_record_dict()
|
||||
if not addr:
|
||||
record = App.treeview.get_record_dict()
|
||||
if not record:
|
||||
return
|
||||
ip = addr["ip"]
|
||||
qport = addr["qport"]
|
||||
data = call_out("show_server_modlist", ip, qport)
|
||||
data = call_out("show_server_modlist", record.ip, str(record.qport))
|
||||
mod_count = self._parse_modlist_rows(data)
|
||||
self.view.set_model(modlist_store)
|
||||
GLib.idle_add(_load)
|
||||
@ -3487,6 +3709,9 @@ class Options(Gtk.Box):
|
||||
[LeftLabel("Force update local mods"), self.force_button, eb2],
|
||||
]
|
||||
|
||||
self.dayz_version_label = Gtk.Label(label="-")
|
||||
self.dayz_exp_version_label = Gtk.Label(label="-")
|
||||
|
||||
self.branch_combo = Gtk.ComboBoxText()
|
||||
self.branch_combo.append_text("Stable")
|
||||
self.branch_combo.append_text("Testing")
|
||||
@ -3499,7 +3724,11 @@ class Options(Gtk.Box):
|
||||
)
|
||||
eb = InfoEventBox(msg)
|
||||
|
||||
version_rows = [[LeftLabel("Branch"), self.branch_combo, eb]]
|
||||
version_rows = [
|
||||
[LeftLabel("DayZ"), self.dayz_version_label],
|
||||
[LeftLabel("DayZ Experimental"), self.dayz_exp_version_label],
|
||||
[LeftLabel("DZGUI branch"), self.branch_combo, eb]
|
||||
]
|
||||
|
||||
api_grid = self._make_grid(api_rows)
|
||||
prefs_grid = self._make_grid(pref_rows)
|
||||
@ -3743,6 +3972,7 @@ class Options(Gtk.Box):
|
||||
name = config_vals[3]
|
||||
client = config_vals[5]
|
||||
fullscreen = config_vals[6]
|
||||
default_steam_path = config_vals[7]
|
||||
|
||||
try:
|
||||
steam = query_config("steam_api")[0]
|
||||
@ -3785,6 +4015,21 @@ class Options(Gtk.Box):
|
||||
if field[0] == "":
|
||||
field[1].get_children()[1].set_sensitive(False)
|
||||
|
||||
try:
|
||||
pe_file_path = PeFile.get_pefile_path(default_steam_path, APPID_DAYZ)
|
||||
dayz_version = PeFile.get_dayz_version_str(pe_file_path)
|
||||
except Exception:
|
||||
dayz_version = "-"
|
||||
|
||||
try:
|
||||
exp_file_path = PeFile.get_pefile_path(default_steam_path, APPID_DAYZ_EXP)
|
||||
dayz_exp_version = PeFile.get_dayz_version_str(exp_file_path)
|
||||
except Exception:
|
||||
dayz_exp_version = "-"
|
||||
|
||||
self.dayz_version_label.set_text(dayz_version)
|
||||
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
||||
|
||||
if branch == "testing":
|
||||
self.branch_combo.set_active(1)
|
||||
else:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user