mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 20:23:11 +02:00
git-subtree-dir: linux_configuration git-subtree-mainline:11427631cdgit-subtree-split:0762e3d07b
664 lines
21 KiB
Bash
Executable File
664 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Steam game compatibility checker for Linux
|
|
#
|
|
# Features:
|
|
# - Gets your games either via Steam Web API (owned library) or by scanning installed appmanifests.
|
|
# - Fetches system requirements from Steam Store API (no key required).
|
|
# - Compares against your system (CPU, RAM, GPU vendor, OS/arch) with simple heuristics.
|
|
# - Ranks games from most to least likely to run.
|
|
#
|
|
# Optional env vars (for full library):
|
|
# STEAM_API_KEY - Your Steam Web API key
|
|
# STEAM_ID64 - Your 64-bit Steam ID
|
|
#
|
|
# Dependencies: curl, jq, awk, sed, grep, sort, lspci, free, uname
|
|
# Recommended: timeout (coreutils) to guard slow network calls
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME=${0##*/}
|
|
ABORT=0
|
|
on_abort() {
|
|
ABORT=1
|
|
log "Aborted by user"
|
|
exit 130
|
|
}
|
|
trap on_abort INT TERM
|
|
|
|
# --------------------------- CLI args ---------------------------
|
|
|
|
usage() {
|
|
cat << USAGE
|
|
Usage: $SCRIPT_NAME [--refresh] [--clear-cache] [--verbose] [--help]
|
|
|
|
Options:
|
|
--refresh Re-analyze all games and overwrite cache (ignore previous results).
|
|
--clear-cache Delete cached results before running (implies --refresh).
|
|
-v, --verbose Print detailed progress and HTTP/parse steps.
|
|
-h, --help Show this help message and exit.
|
|
USAGE
|
|
}
|
|
|
|
FORCE_REFRESH=0
|
|
CLEAR_CACHE=0
|
|
VERBOSE=0
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--refresh)
|
|
FORCE_REFRESH=1
|
|
shift
|
|
;;
|
|
--clear-cache)
|
|
CLEAR_CACHE=1
|
|
FORCE_REFRESH=1
|
|
shift
|
|
;;
|
|
-v | --verbose)
|
|
VERBOSE=1
|
|
shift
|
|
;;
|
|
-h | --help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "Unknown option: $1 (use --help)"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
log() { printf "[%s] %s\n" "$SCRIPT_NAME" "$*" >&2; }
|
|
die() {
|
|
printf "[%s] ERROR: %s\n" "$SCRIPT_NAME" "$*" >&2
|
|
exit 1
|
|
}
|
|
vlog() { if [[ $VERBOSE -eq 1 ]]; then printf "[%s][verbose] %s\n" "$SCRIPT_NAME" "$*" >&2; fi; }
|
|
|
|
require_cmd() {
|
|
command -v "$1" > /dev/null 2>&1 || die "Missing dependency: $1"
|
|
}
|
|
|
|
for cmd in curl jq awk sed grep sort lspci free uname; do
|
|
require_cmd "$cmd"
|
|
done
|
|
|
|
HAS_TIMEOUT=0
|
|
if command -v timeout > /dev/null 2>&1; then
|
|
HAS_TIMEOUT=1
|
|
fi
|
|
|
|
# Safe HTTP GET with optional timeout if available
|
|
http_get() {
|
|
local url="$1"
|
|
shift || true
|
|
local ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36"
|
|
if [[ $HAS_TIMEOUT -eq 1 ]]; then
|
|
timeout 12s curl -sSL --compressed --retry 2 --retry-delay 0.5 --retry-connrefused \
|
|
-H "User-Agent: $ua" -H 'Accept: application/json, text/plain, */*' "$url" "$@" 2> /dev/null
|
|
else
|
|
curl -sSL --compressed --retry 2 --retry-delay 0.5 --retry-connrefused \
|
|
-H "User-Agent: $ua" -H 'Accept: application/json, text/plain, */*' "$url" "$@" 2> /dev/null
|
|
fi
|
|
}
|
|
|
|
# --------------------------- System detection ---------------------------
|
|
|
|
SYSTEM_CPU_MODEL=""
|
|
SYSTEM_CPU_CLASS="unknown"
|
|
SYSTEM_GPU_VENDOR="unknown"
|
|
SYSTEM_RAM_GB=0
|
|
SYSTEM_ARCH="$(uname -m || echo unknown)"
|
|
|
|
to_int() { awk '{gsub(/[^0-9]/,""); if($0=="") print 0; else print $0}' <<< "$1"; }
|
|
|
|
detect_system() {
|
|
# CPU model
|
|
if command -v lscpu > /dev/null 2>&1; then
|
|
SYSTEM_CPU_MODEL=$(lscpu | awk -F': *' '/Model name/ {print $2; exit}')
|
|
fi
|
|
if [[ -z $SYSTEM_CPU_MODEL && -r /proc/cpuinfo ]]; then
|
|
SYSTEM_CPU_MODEL=$(awk -F': *' '/model name/ {print $2; exit}' /proc/cpuinfo)
|
|
fi
|
|
SYSTEM_CPU_MODEL=${SYSTEM_CPU_MODEL:-unknown}
|
|
|
|
# CPU class (very rough)
|
|
lc_model=$(tr '[:upper:]' '[:lower:]' <<< "$SYSTEM_CPU_MODEL")
|
|
if grep -qiE 'i9-|core\(tm\) i9| ryzen 9' <<< "$lc_model"; then
|
|
SYSTEM_CPU_CLASS="tier4"
|
|
elif grep -qiE 'i7-|core\(tm\) i7| ryzen 7' <<< "$lc_model"; then
|
|
SYSTEM_CPU_CLASS="tier3"
|
|
elif grep -qiE 'i5-|core\(tm\) i5| ryzen 5' <<< "$lc_model"; then
|
|
SYSTEM_CPU_CLASS="tier2"
|
|
elif grep -qiE 'i3-|core\(tm\) i3| ryzen 3| pentium|celeron|atom' <<< "$lc_model"; then
|
|
SYSTEM_CPU_CLASS="tier1"
|
|
else SYSTEM_CPU_CLASS="tier2"; fi
|
|
|
|
# GPU vendor
|
|
local vga
|
|
vga=$(lspci 2> /dev/null | grep -iE 'vga|3d|display' | head -n1 || true)
|
|
lc_vga=$(tr '[:upper:]' '[:lower:]' <<< "$vga")
|
|
if grep -q 'nvidia' <<< "$lc_vga"; then
|
|
SYSTEM_GPU_VENDOR="nvidia"
|
|
elif grep -q -E 'amd|ati|radeon' <<< "$lc_vga"; then
|
|
SYSTEM_GPU_VENDOR="amd"
|
|
elif grep -q 'intel' <<< "$lc_vga"; then
|
|
SYSTEM_GPU_VENDOR="intel"
|
|
else SYSTEM_GPU_VENDOR="unknown"; fi
|
|
|
|
# RAM GB
|
|
local mem_kb
|
|
mem_kb=$(awk '/MemTotal/ {print $2; exit}' /proc/meminfo 2> /dev/null || echo 0)
|
|
if [[ $mem_kb -gt 0 ]]; then
|
|
SYSTEM_RAM_GB=$(((mem_kb + 1023 * 1024) / (1024 * 1024)))
|
|
else
|
|
local mem_mb
|
|
mem_mb=$(free -m | awk '/Mem:/ {print $2; exit}')
|
|
SYSTEM_RAM_GB=$(((mem_mb + 1023) / 1024))
|
|
fi
|
|
}
|
|
|
|
cpu_class_rank() {
|
|
case "$1" in
|
|
tier1) echo 1 ;;
|
|
tier2) echo 2 ;;
|
|
tier3) echo 3 ;;
|
|
tier4) echo 4 ;;
|
|
*) echo 2 ;;
|
|
esac
|
|
}
|
|
|
|
required_cpu_rank_from_text() {
|
|
local t
|
|
t=$(tr '[:upper:]' '[:lower:]' <<< "$1")
|
|
if grep -qE 'i9|ryzen 9' <<< "$t"; then
|
|
echo 4
|
|
return
|
|
fi
|
|
if grep -qE 'i7|ryzen 7' <<< "$t"; then
|
|
echo 3
|
|
return
|
|
fi
|
|
if grep -qE 'i5|ryzen 5' <<< "$t"; then
|
|
echo 2
|
|
return
|
|
fi
|
|
if grep -qE 'i3|ryzen 3|pentium|celeron|atom' <<< "$t"; then
|
|
echo 1
|
|
return
|
|
fi
|
|
echo 2
|
|
}
|
|
|
|
gpu_vendor_required_from_text() {
|
|
local t
|
|
t=$(tr '[:upper:]' '[:lower:]' <<< "$1")
|
|
if grep -qE 'nvidia|geforce|gtx|rtx' <<< "$t"; then
|
|
echo nvidia
|
|
return
|
|
fi
|
|
if grep -qE 'amd|radeon|rx[ -]?[0-9]' <<< "$t"; then
|
|
echo amd
|
|
return
|
|
fi
|
|
if grep -qE 'intel( graphics| arc| iris| hd)' <<< "$t"; then
|
|
echo intel
|
|
return
|
|
fi
|
|
echo unknown
|
|
}
|
|
|
|
strip_html() {
|
|
sed -E 's/<[^>]+>//g; s/ / /g; s/&/\&/g; s/\r//g' <<< "$1"
|
|
}
|
|
|
|
parse_ram_gb() {
|
|
# Extract first RAM mention and convert to GB integer
|
|
local text="$1"
|
|
local num val
|
|
# Prefer GB
|
|
num=$(grep -oiE '([0-9]+)\s*(gb|gib)' <<< "$text" | head -n1 | grep -oiE '^[0-9]+' || true)
|
|
if [[ -n $num ]]; then
|
|
echo "$num"
|
|
return
|
|
fi
|
|
# Try MB
|
|
num=$(grep -oiE '([0-9]+)\s*(mb|mib)' <<< "$text" | head -n1 | grep -oiE '^[0-9]+' || true)
|
|
if [[ -n $num ]]; then
|
|
val=$(((num + 1023) / 1024))
|
|
echo "$val"
|
|
return
|
|
fi
|
|
echo 0
|
|
}
|
|
|
|
# --------------------------- Steam data ---------------------------
|
|
|
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/steam-compat-check"
|
|
CONFIG_FILE="$CONFIG_DIR/credentials.conf"
|
|
|
|
# Cache for analyzed results
|
|
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/steam-compat-check"
|
|
RESULTS_CACHE="$CACHE_DIR/results.tsv"
|
|
|
|
load_credentials() {
|
|
# Prefer environment, else config file
|
|
if [[ -n ${STEAM_API_KEY:-} && -n ${STEAM_ID64:-} ]]; then
|
|
return 0
|
|
fi
|
|
if [[ -r $CONFIG_FILE ]]; then
|
|
# shellcheck disable=SC1090
|
|
. "$CONFIG_FILE" || true
|
|
fi
|
|
if [[ -z ${STEAM_API_KEY:-} || -z ${STEAM_ID64:-} ]]; then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
save_credentials() {
|
|
local key="$1" id="$2"
|
|
mkdir -p "$CONFIG_DIR"
|
|
chmod 700 "$CONFIG_DIR" 2> /dev/null || true
|
|
umask 177
|
|
cat > "$CONFIG_FILE" << EOF
|
|
# Saved by $SCRIPT_NAME
|
|
STEAM_API_KEY="$key"
|
|
STEAM_ID64="$id"
|
|
EOF
|
|
}
|
|
|
|
prompt_for_credentials() {
|
|
if [[ ! -t 0 ]]; then
|
|
die "STEAM_API_KEY/STEAM_ID64 not set and input is non-interactive. Export them or create $CONFIG_FILE."
|
|
fi
|
|
echo "Steam Web API credentials are required to scan your full library."
|
|
echo
|
|
echo "Where to get them:"
|
|
echo "- Steam Web API Key: https://steamcommunity.com/dev/apikey"
|
|
echo " Log in, set any domain (e.g., 127.0.0.1), then copy the key."
|
|
echo "- SteamID64 (17-digit ID starting with 765):"
|
|
echo " * Easiest: https://steamid.io/ (paste your profile URL to get the 64-bit ID)"
|
|
echo " * Or enable URL bar in Steam (Settings > Interface), open your profile; the URL contains the ID."
|
|
echo
|
|
local key id
|
|
read -r -p "Enter Steam Web API Key: " key
|
|
read -r -p "Enter Steam 64-bit ID (begins with 765…): " id
|
|
if [[ -z $key || -z $id ]]; then
|
|
die "Credentials not provided. Exiting."
|
|
fi
|
|
# Light validation for ID64
|
|
if ! grep -qE '^765[0-9]{14}$' <<< "$id"; then
|
|
log "Warning: Steam ID64 format unexpected; continuing anyway."
|
|
fi
|
|
STEAM_API_KEY="$key"
|
|
STEAM_ID64="$id"
|
|
export STEAM_API_KEY STEAM_ID64
|
|
save_credentials "$STEAM_API_KEY" "$STEAM_ID64"
|
|
log "Saved credentials to $CONFIG_FILE"
|
|
}
|
|
|
|
STEAM_DIRS=("$HOME/.steam/steam" "$HOME/.local/share/Steam")
|
|
|
|
find_steamapps_dirs() {
|
|
local dirs=()
|
|
for base in "${STEAM_DIRS[@]}"; do
|
|
[[ -d $base ]] || continue
|
|
if [[ -f "$base/steamapps/libraryfolders.vdf" ]]; then
|
|
# Newer format includes nested objects with paths
|
|
local paths
|
|
paths=$(grep -oE '"path"\s+"[^"]+"' "$base/steamapps/libraryfolders.vdf" | sed -E 's/.*"([^"]+)"/\1/' || true)
|
|
if [[ -n $paths ]]; then
|
|
while IFS= read -r p; do
|
|
[[ -d "$p/steamapps" ]] && dirs+=("$p/steamapps")
|
|
done <<< "$paths"
|
|
fi
|
|
fi
|
|
[[ -d "$base/steamapps" ]] && dirs+=("$base/steamapps")
|
|
done
|
|
# de-dupe
|
|
printf "%s\n" "${dirs[@]}" 2> /dev/null | awk '!seen[$0]++'
|
|
}
|
|
|
|
list_installed_games() {
|
|
local d appid name
|
|
while IFS= read -r d; do
|
|
[[ -d $d ]] || continue
|
|
for mf in "$d"/appmanifest_*.acf; do
|
|
[[ -f $mf ]] || continue
|
|
appid=$(grep -oE '"appid"\s+"[0-9]+"' "$mf" | sed -E 's/.*"([0-9]+)"/\1/' | head -n1)
|
|
name=$(grep -oE '"name"\s+"[^"]+"' "$mf" | sed -E 's/.*"([^"]+)"/\1/' | head -n1)
|
|
if [[ -n $appid ]]; then
|
|
printf "%s\t%s\n" "$appid" "${name:-Unknown}"
|
|
fi
|
|
done
|
|
done < <(find_steamapps_dirs)
|
|
}
|
|
|
|
list_owned_games_via_api() {
|
|
local key="${STEAM_API_KEY:-}" sid="${STEAM_ID64:-}"
|
|
if [[ -z $key || -z $sid ]]; then
|
|
return 1
|
|
fi
|
|
local url="https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=${key}&steamid=${sid}&include_appinfo=1&include_played_free_games=1&format=json"
|
|
http_get "$url" | jq -r '.response.games[]? | "\(.appid)\t\(.name)"' || return 1
|
|
}
|
|
|
|
fetch_appdetails_json() {
|
|
local appid="$1"
|
|
# Using store API (no key)
|
|
local url="https://store.steampowered.com/api/appdetails?appids=${appid}&l=en&cc=us"
|
|
http_get "$url" || true
|
|
}
|
|
|
|
extract_requirements_and_platforms() {
|
|
# Input: JSON from appdetails; Output: TSV of fields
|
|
# Fields: success, linux, windows, mac, min_text, rec_text, type
|
|
local appid="$1" json="$2"
|
|
# Some apps return {"APPID": {"success":true, "data":{...}}}
|
|
local out
|
|
out=$(jq -r --arg APP "$appid" '
|
|
.[$APP] as $root | if ($root.success==true and ($root.data|type)=="object") then
|
|
($root.data.platforms.linux // false) as $linux |
|
|
($root.data.platforms.windows // false) as $windows |
|
|
($root.data.platforms.mac // false) as $mac |
|
|
($root.data.type // "") as $type |
|
|
# Prefer Linux reqs when present
|
|
($root.data.linux_requirements.minimum // $root.data.pc_requirements.minimum // "") as $min |
|
|
($root.data.linux_requirements.recommended // $root.data.pc_requirements.recommended // "") as $rec |
|
|
["ok", ($linux|tostring), ($windows|tostring), ($mac|tostring), ($min|tostring), ($rec|tostring), ($type|tostring)] | @tsv
|
|
else
|
|
["fail", "false", "false", "false", "", "", ""] | @tsv
|
|
end' 2> /dev/null <<< "$json") || true
|
|
if [[ -z $out ]]; then
|
|
out=$'fail false false false '
|
|
fi
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
# Read JSON from stdin (avoids storing large/binary data in variables)
|
|
extract_requirements_and_platforms_stdin() {
|
|
local appid="$1"
|
|
local out
|
|
out=$(jq -r --arg APP "$appid" '
|
|
.[$APP] as $root | if ($root.success==true and ($root.data|type)=="object") then
|
|
($root.data.platforms.linux // false) as $linux |
|
|
($root.data.platforms.windows // false) as $windows |
|
|
($root.data.platforms.mac // false) as $mac |
|
|
($root.data.type // "") as $type |
|
|
($root.data.linux_requirements.minimum // $root.data.pc_requirements.minimum // "") as $min |
|
|
($root.data.linux_requirements.recommended // $root.data.pc_requirements.recommended // "") as $rec |
|
|
["ok", ($linux|tostring), ($windows|tostring), ($mac|tostring), ($min|tostring), ($rec|tostring), ($type|tostring)] | @tsv
|
|
else
|
|
["fail", "false", "false", "false", "", "", ""] | @tsv
|
|
end' 2> /dev/null) || true
|
|
if [[ -z $out ]]; then
|
|
out=$'fail\tfalse\tfalse\tfalse\t\t\t'
|
|
fi
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
score_game() {
|
|
local linux_support="$1" min_txt="$2" rec_txt="$3"
|
|
local score=0
|
|
|
|
# Linux platform support
|
|
if [[ $linux_support == "true" ]]; then
|
|
score=$((score + 50))
|
|
else
|
|
score=$((score + 20)) # Assume Proton potential
|
|
fi
|
|
|
|
local min_plain rec_plain
|
|
min_plain=$(strip_html "$min_txt")
|
|
rec_plain=$(strip_html "$rec_txt")
|
|
|
|
local min_ram rec_ram
|
|
min_ram=$(parse_ram_gb "$min_plain")
|
|
rec_ram=$(parse_ram_gb "$rec_plain")
|
|
|
|
# RAM checks
|
|
if [[ $min_ram -gt 0 ]]; then
|
|
if [[ $SYSTEM_RAM_GB -ge $min_ram ]]; then score=$((score + 15)); else score=$((score - 30)); fi
|
|
fi
|
|
if [[ $rec_ram -gt 0 ]]; then
|
|
if [[ $SYSTEM_RAM_GB -ge $rec_ram ]]; then score=$((score + 10)); else score=$((score - 10)); fi
|
|
fi
|
|
|
|
# CPU checks (very rough tiers)
|
|
local req_rank sys_rank
|
|
req_rank=$(required_cpu_rank_from_text "$min_plain $rec_plain")
|
|
sys_rank=$(cpu_class_rank "$SYSTEM_CPU_CLASS")
|
|
if [[ $sys_rank -ge $req_rank ]]; then score=$((score + 10)); else score=$((score - 10)); fi
|
|
|
|
# GPU vendor hints
|
|
local req_gpu vendor
|
|
req_gpu=$(gpu_vendor_required_from_text "$min_plain $rec_plain")
|
|
vendor="$SYSTEM_GPU_VENDOR"
|
|
if [[ $req_gpu == "unknown" ]]; then
|
|
score=$((score + 5))
|
|
elif [[ $req_gpu == "$vendor" ]]; then
|
|
score=$((score + 10))
|
|
else
|
|
score=$((score - 10))
|
|
fi
|
|
|
|
# 64-bit OS requirement
|
|
if grep -qi '64-?bit' <<< "$min_plain $rec_plain"; then
|
|
if [[ $SYSTEM_ARCH == "x86_64" || $SYSTEM_ARCH == "aarch64" ]]; then
|
|
score=$((score + 5))
|
|
else
|
|
score=$((score - 20))
|
|
fi
|
|
fi
|
|
|
|
printf "%s\t%s\t%s\n" "$score" "$min_ram" "$rec_ram"
|
|
}
|
|
|
|
print_header() {
|
|
printf "%-5s %-8s %-6s %-6s %-8s %-9s %s\n" "Rank" "Score" "MinRAM" "RecRAM" "Linux" "ProtonDB" "Title"
|
|
}
|
|
|
|
check_network_or_exit() {
|
|
# Quick probe to Steam Store API; exit early if not reachable
|
|
local probe_url="https://store.steampowered.com/api/appdetails?appids=10&l=en&cc=us"
|
|
if ! http_get "$probe_url" | jq -e '."10".success == true' > /dev/null 2>&1; then
|
|
log "Warning: store.steampowered.com probe failed (network or rate-limit). Continuing and handling per-app."
|
|
fi
|
|
}
|
|
|
|
is_known_tool_name() {
|
|
local name_lc
|
|
name_lc=$(tr '[:upper:]' '[:lower:]' <<< "$1")
|
|
if grep -qE 'steam linux runtime|proton|compatibility tool' <<< "$name_lc"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
ensure_cache_dir() {
|
|
mkdir -p "$CACHE_DIR" 2> /dev/null || true
|
|
}
|
|
|
|
declare -A CACHE_MAP
|
|
load_cache_map() {
|
|
CACHE_MAP=()
|
|
if [[ -r $RESULTS_CACHE ]]; then
|
|
while IFS= read -r raw_line; do
|
|
# Normalize historical caches that contain literal "\t" instead of real tabs
|
|
local norm_line
|
|
norm_line=$(printf "%s" "$raw_line" | sed -E $'s/\\t/\t/g; s/\r$//')
|
|
IFS=$'\t' read -r c_score c_appid c_linux c_min c_rec c_name c_pdb <<< "$norm_line"
|
|
[[ -z ${c_appid:-} ]] && continue
|
|
c_pdb=${c_pdb:-unknown}
|
|
CACHE_MAP["$c_appid"]="$c_score\t$c_appid\t$c_linux\t$c_min\t$c_rec\t$c_name\t$c_pdb"
|
|
done < "$RESULTS_CACHE"
|
|
fi
|
|
}
|
|
|
|
# --------------------------- ProtonDB integration ---------------------------
|
|
|
|
fetch_protondb_tier() {
|
|
local appid="$1"
|
|
local url="https://www.protondb.com/api/v1/reports/summaries/${appid}.json"
|
|
# Returns minimal JSON including .tier, .confidence; we only need .tier
|
|
local tier
|
|
tier=$(http_get "$url" | jq -r 'try .tier // "unknown"' 2> /dev/null || true)
|
|
if [[ -z $tier || $tier == "null" ]]; then
|
|
echo "unknown"
|
|
else
|
|
tr '[:upper:]' '[:lower:]' <<< "$tier" | tr -d '\r\n'
|
|
fi
|
|
}
|
|
|
|
protondb_allowed() {
|
|
local tier
|
|
tier=$(tr '[:upper:]' '[:lower:]' <<< "${1:-}")
|
|
case "$tier" in
|
|
platinum | native | gold | silver | unknown | "") return 0 ;;
|
|
bronze | pending | borked | unsupported | broken) return 1 ;;
|
|
*) return 0 ;;
|
|
esac
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
detect_system
|
|
log "System: CPU=[$SYSTEM_CPU_MODEL] class=$SYSTEM_CPU_CLASS | GPU=$SYSTEM_GPU_VENDOR | RAM=${SYSTEM_RAM_GB}GB | Arch=$SYSTEM_ARCH"
|
|
|
|
local tmpdir
|
|
tmpdir=$(mktemp -d)
|
|
trap '[[ -n "${tmpdir:-}" ]] && rm -rf "$tmpdir"' EXIT
|
|
|
|
local games_tsv="$tmpdir/games.tsv"
|
|
: > "$games_tsv"
|
|
|
|
# Ensure credentials exist: load from env/config or prompt, else exit
|
|
if ! load_credentials; then
|
|
prompt_for_credentials
|
|
fi
|
|
|
|
# Fail fast if we cannot reach the store API to avoid noisy per-app errors
|
|
check_network_or_exit
|
|
|
|
if list_owned_games_via_api > "$games_tsv" 2> /dev/null; then
|
|
log "Fetched owned games via Steam Web API"
|
|
fi
|
|
|
|
if [[ ! -s $games_tsv ]]; then
|
|
die "No games found from Steam Web API. Check STEAM_API_KEY/STEAM_ID64 and network connectivity."
|
|
fi
|
|
|
|
# Fail fast if we cannot reach the store API to avoid noisy per-app errors
|
|
check_network_or_exit
|
|
|
|
ensure_cache_dir
|
|
if [[ $CLEAR_CACHE -eq 1 ]] && [[ -f $RESULTS_CACHE ]]; then
|
|
rm -f "$RESULTS_CACHE" || true
|
|
log "Cleared cache: $RESULTS_CACHE"
|
|
fi
|
|
if [[ $FORCE_REFRESH -eq 0 ]]; then
|
|
load_cache_map
|
|
else
|
|
CACHE_MAP=()
|
|
fi
|
|
|
|
local results_combined="$tmpdir/results.tsv"
|
|
: > "$results_combined"
|
|
|
|
local count=0
|
|
local total
|
|
total=$(wc -l < "$games_tsv" | tr -d ' ')
|
|
[[ -z $total ]] && total=0
|
|
while IFS=$'\t' read -r appid name; do
|
|
[[ $ABORT -eq 1 ]] && break
|
|
[[ -n $appid ]] || continue
|
|
if is_known_tool_name "$name"; then
|
|
vlog "[$((count + 1))/$total] Skipping compatibility tool: $name ($appid)"
|
|
continue
|
|
fi
|
|
# If cached, reuse; else analyze and cache
|
|
if [[ $FORCE_REFRESH -eq 0 && -n ${CACHE_MAP[$appid]+isset} ]]; then
|
|
# Normalize to include ProtonDB column if older cache lacked it
|
|
IFS=$'\t' read -r c_score c_a c_linux c_min c_rec c_name c_pdb <<< "${CACHE_MAP[$appid]}"
|
|
c_pdb=${c_pdb:-unknown}
|
|
vlog "[$((count + 1))/$total] Cache hit: $name ($appid) | ProtonDB=$c_pdb"
|
|
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$c_score" "$c_a" "$c_linux" "$c_min" "$c_rec" "$c_name" "$c_pdb" >> "$results_combined"
|
|
continue
|
|
fi
|
|
count=$((count + 1))
|
|
log "Analyzing: $name ($appid) [$count/$total]"
|
|
local row url
|
|
url="https://store.steampowered.com/api/appdetails?appids=${appid}&l=en&cc=us"
|
|
vlog "[$count/$total] Fetching store appdetails: $url"
|
|
# Be gentle with the store API
|
|
sleep 0.1
|
|
row=$(http_get "$url" | extract_requirements_and_platforms_stdin "$appid" || true)
|
|
if [[ -z $row ]]; then continue; fi
|
|
local status linux _windows _mac min_txt rec_txt type
|
|
IFS=$'\t' read -r status linux _windows _mac min_txt rec_txt type <<< "$row"
|
|
vlog "[$count/$total] Parsed store data: status=$status linux=$linux type=$type"
|
|
# Occasionally Steam returns success=false spuriously; retry once
|
|
if [[ $status != "ok" ]]; then
|
|
vlog "[$count/$total] Store status=fail; retrying once..."
|
|
sleep 0.3
|
|
row=$(http_get "$url" | extract_requirements_and_platforms_stdin "$appid" || true)
|
|
IFS=$'\t' read -r status linux _windows _mac min_txt rec_txt type <<< "$row"
|
|
vlog "[$count/$total] After retry: status=$status"
|
|
fi
|
|
# Try filtered endpoint that often bypasses age/region gates
|
|
if [[ $status != "ok" ]]; then
|
|
local url2="https://store.steampowered.com/api/appdetails?appids=${appid}&filters=platforms,linux_requirements,pc_requirements,type&l=en&cc=us"
|
|
vlog "[$count/$total] Retrying with filters: $url2"
|
|
sleep 0.1
|
|
row=$(http_get "$url2" | extract_requirements_and_platforms_stdin "$appid" || true)
|
|
IFS=$'\t' read -r status linux _windows _mac min_txt rec_txt type <<< "$row"
|
|
vlog "[$count/$total] Filtered fetch status=$status"
|
|
fi
|
|
if [[ $status != "ok" ]]; then continue; fi
|
|
if [[ $type != "game" && $type != "dlc" && $type != "" ]]; then continue; fi
|
|
# ProtonDB tier
|
|
[[ $ABORT -eq 1 ]] && break
|
|
local pdb_tier
|
|
vlog "[$count/$total] Fetching ProtonDB tier for appid=$appid"
|
|
pdb_tier=$(fetch_protondb_tier "$appid")
|
|
vlog "[$count/$total] ProtonDB tier=$pdb_tier"
|
|
|
|
# Compute hardware-based score
|
|
local score_line s_score s_min_ram s_rec_ram
|
|
score_line=$(score_game "$linux" "$min_txt" "$rec_txt")
|
|
IFS=$'\t' read -r s_score s_min_ram s_rec_ram <<< "$score_line"
|
|
|
|
# Gate by ProtonDB: if bronze or below -> mark unplayable and force low score
|
|
if ! protondb_allowed "$pdb_tier"; then
|
|
s_score=-999
|
|
fi
|
|
|
|
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$s_score" "$appid" "$linux" "$s_min_ram" "$s_rec_ram" "$name" "$pdb_tier" >> "$results_combined"
|
|
vlog "[$count/$total] Scored and recorded: score=$s_score min=${s_min_ram}G rec=${s_rec_ram}G"
|
|
done < "$games_tsv"
|
|
|
|
if [[ ! -s $results_combined ]]; then
|
|
die "No compatible entries parsed from store API."
|
|
fi
|
|
|
|
print_header
|
|
local rank=0
|
|
sort -t $'\t' -k1,1nr -k6,6 "$results_combined" | while IFS=$'\t' read -r score appid linux min_ram rec_ram name pdb_tier; do
|
|
rank=$((rank + 1))
|
|
local display_name="$name"
|
|
if ! protondb_allowed "$pdb_tier"; then
|
|
display_name="$name [UNPLAYABLE]"
|
|
fi
|
|
printf "%-5s %-8s %-6s %-6s %-8s %-9s %s\n" "$rank" "$score" "${min_ram}G" "${rec_ram}G" "$linux" "${pdb_tier:-unknown}" "$display_name"
|
|
done
|
|
|
|
# Persist updated results for future runs (only current library entries)
|
|
cp -f "$results_combined" "$RESULTS_CACHE" 2> /dev/null || cat "$results_combined" > "$RESULTS_CACHE"
|
|
}
|
|
|
|
main "$@"
|