mirror of
https://github.com/seuyh/stellaris-dlc-unlocker.git
synced 2026-07-28 14:47:05 -07:00
1002 lines
32 KiB
Bash
1002 lines
32 KiB
Bash
#!/bin/bash
|
||
set -uo pipefail
|
||
|
||
APPID="281990"
|
||
APP_DIR="$HOME/.local/share/StellarisDLCUnlocker"
|
||
CACHE_DIR="$APP_DIR/cache"
|
||
LOG_FILE="$APP_DIR/unlocker.log"
|
||
CONFIG_FILE="$APP_DIR/config.ini"
|
||
|
||
REPO_GITHUB_RAW="https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/main"
|
||
REPO_JSDELIVR="https://cdn.jsdelivr.net/gh/seuyh/stellaris-dlc-unlocker@main"
|
||
|
||
GITHUB_DLC_URL="$REPO_GITHUB_RAW/dlc_data.json"
|
||
JSDELIVR_DLC_URL="$REPO_JSDELIVR/dlc_data.json"
|
||
GITHUB_HASHES_URL="$REPO_GITHUB_RAW/hashes.json"
|
||
JSDELIVR_HASHES_URL="$REPO_JSDELIVR/hashes.json"
|
||
SERVER_URL="yblocker.xyz"
|
||
STEAMCMD_API="https://api.steamcmd.net/v1/info"
|
||
|
||
CREAMLINUX_SUBDIR="creamlinux"
|
||
CREAMLINUX_FILES=(cream.sh lib32Creamlinux.so lib64Creamlinux.so cream_api.ini)
|
||
|
||
mkdir -p "$APP_DIR" "$CACHE_DIR"
|
||
|
||
C_RESET=$'\e[0m'; C_BOLD=$'\e[1m'; C_DIM=$'\e[2m'
|
||
C_RED=$'\e[31m'; C_GREEN=$'\e[32m'; C_YELLOW=$'\e[33m'
|
||
C_BLUE=$'\e[34m'; C_MAGENTA=$'\e[35m'; C_CYAN=$'\e[36m'; C_WHITE=$'\e[97m'
|
||
|
||
LANGUAGE="en"
|
||
|
||
t() {
|
||
local key="$1"
|
||
case "$LANGUAGE" in
|
||
ru) _t_ru "$key" ;;
|
||
zh) _t_zh "$key" ;;
|
||
*) _t_en "$key" ;;
|
||
esac
|
||
}
|
||
|
||
_t_en() {
|
||
case "$1" in
|
||
title) echo "Stellaris DLC Unlocker" ;;
|
||
menu_header) echo "MAIN MENU" ;;
|
||
m_status) echo "Status" ;;
|
||
m_install) echo "Install / Update" ;;
|
||
fetching_dlc_info) echo "Fetching DLC info:" ;;
|
||
fetching_file) echo " -> In progress:" ;;
|
||
m_steam_launch) echo "Set Steam launch options" ;;
|
||
m_lang) echo "Language" ;;
|
||
m_log) echo "Log" ;;
|
||
m_exit) echo "Exit" ;;
|
||
choose) echo "> " ;;
|
||
steam_found) echo "Steam:" ;;
|
||
steam_not_found) echo "Steam not found." ;;
|
||
game_found) echo "Stellaris:" ;;
|
||
game_not_found) echo "Stellaris not found." ;;
|
||
native_build) echo "Native Linux build." ;;
|
||
not_native) echo "Not a native Linux build. Use the Proton unlocker instead." ;;
|
||
downloading) echo "Downloading" ;;
|
||
fetching_dlc_list) echo "Fetching DLC list..." ;;
|
||
dlc_list_ok) echo "DLC list loaded." ;;
|
||
dlc_list_fail) echo "Failed to fetch DLC list." ;;
|
||
fetching_creamlinux) echo "Downloading CreamLinux files..." ;;
|
||
creamlinux_ok) echo "CreamLinux files ready." ;;
|
||
copying_files) echo "Copying files to game folder..." ;;
|
||
to_download) echo "DLC to download:" ;;
|
||
unpacking) echo "Unpacking..." ;;
|
||
updating_ini) echo "Updating cream_api.ini..." ;;
|
||
ini_updated) echo "cream_api.ini: added" ;;
|
||
ini_skip) echo "SteamCMD unavailable, skipped." ;;
|
||
install_done) echo "Done." ;;
|
||
patch_launch_header) echo "Steam launch options" ;;
|
||
patch_userdata_not_found) echo "Steam userdata folder not found." ;;
|
||
patch_localconfig_not_found) echo "localconfig.vdf not found." ;;
|
||
patch_backup) echo "Backup:" ;;
|
||
patch_done) echo "Launch options set." ;;
|
||
steam_running_prompt) echo "Steam is running and must be closed to apply launch options. Close it now? [Y/n]: " ;;
|
||
steam_closing) echo "Closing Steam..." ;;
|
||
steam_close_ok) echo "Steam closed." ;;
|
||
steam_close_fail) echo "Could not close Steam, aborting." ;;
|
||
abort_steam_running) echo "Skipped: Steam is still running." ;;
|
||
press_enter) echo "Press Enter to continue..." ;;
|
||
enter_game_path) echo "Game path (or Enter to cancel): " ;;
|
||
invalid_game_path) echo "Path does not exist." ;;
|
||
enter_steam_path) echo "Steam not auto-detected. Enter Steam folder path (or Enter to cancel): " ;;
|
||
invalid_steam_path) echo "Not a valid Steam folder (no 'steamapps' inside)." ;;
|
||
invalid_choice) echo "Invalid choice." ;;
|
||
log_empty) echo "Log is empty." ;;
|
||
confirm_install) echo "Proceed? [Y/n]: " ;;
|
||
cancelled) echo "Cancelled." ;;
|
||
no_dlc_to_download) echo "All DLC up to date." ;;
|
||
hashes_skip) echo "Could not fetch hashes.json, skipping integrity check." ;;
|
||
dlc_outdated) echo "Outdated content, will re-download:" ;;
|
||
flatpak_steam) echo "(Flatpak)" ;;
|
||
*) echo "$1" ;;
|
||
esac
|
||
}
|
||
|
||
_t_ru() {
|
||
case "$1" in
|
||
title) echo "Stellaris DLC Unlocker" ;;
|
||
menu_header) echo "ГЛАВНОЕ МЕНЮ" ;;
|
||
m_status) echo "Статус" ;;
|
||
m_install) echo "Установить / обновить" ;;
|
||
fetching_dlc_info) echo "Запрос данных DLC:" ;;
|
||
fetching_file) echo " -> В процессе:" ;;
|
||
m_steam_launch) echo "Прописать параметры запуска Steam" ;;
|
||
m_lang) echo "Язык" ;;
|
||
m_log) echo "Лог" ;;
|
||
m_exit) echo "Выход" ;;
|
||
choose) echo "> " ;;
|
||
steam_found) echo "Steam:" ;;
|
||
steam_not_found) echo "Steam не найден." ;;
|
||
game_found) echo "Stellaris:" ;;
|
||
game_not_found) echo "Stellaris не найден." ;;
|
||
native_build) echo "Нативная Linux-версия." ;;
|
||
not_native) echo "Не нативная Linux-версия. Используйте анлокер для Proton." ;;
|
||
downloading) echo "Скачивание" ;;
|
||
fetching_dlc_list) echo "Получение списка DLC..." ;;
|
||
dlc_list_ok) echo "Список DLC загружен." ;;
|
||
dlc_list_fail) echo "Не удалось получить список DLC." ;;
|
||
fetching_creamlinux) echo "Скачивание файлов CreamLinux..." ;;
|
||
creamlinux_ok) echo "Файлы CreamLinux готовы." ;;
|
||
copying_files) echo "Копирование файлов в папку игры..." ;;
|
||
to_download) echo "DLC к скачиванию:" ;;
|
||
unpacking) echo "Распаковка..." ;;
|
||
updating_ini) echo "Обновление cream_api.ini..." ;;
|
||
ini_updated) echo "cream_api.ini: добавлено" ;;
|
||
ini_skip) echo "SteamCMD недоступен, пропущено." ;;
|
||
install_done) echo "Готово." ;;
|
||
patch_launch_header) echo "Параметры запуска Steam" ;;
|
||
patch_userdata_not_found) echo "Папка userdata Steam не найдена." ;;
|
||
patch_localconfig_not_found) echo "localconfig.vdf не найден." ;;
|
||
patch_backup) echo "Бэкап:" ;;
|
||
patch_done) echo "Параметры запуска установлены." ;;
|
||
steam_running_prompt) echo "Steam запущен, для применения параметров запуска его нужно закрыть. Закрыть сейчас? [Y/n]: " ;;
|
||
steam_closing) echo "Закрытие Steam..." ;;
|
||
steam_close_ok) echo "Steam закрыт." ;;
|
||
steam_close_fail) echo "Не удалось закрыть Steam, отмена." ;;
|
||
abort_steam_running) echo "Пропущено: Steam всё ещё запущен." ;;
|
||
press_enter) echo "Нажмите Enter для продолжения..." ;;
|
||
enter_game_path) echo "Путь к игре (или Enter, чтобы отменить): " ;;
|
||
invalid_game_path) echo "Такого пути не существует." ;;
|
||
enter_steam_path) echo "Steam не найден автоматически. Введите путь к папке Steam (или Enter, чтобы отменить): " ;;
|
||
invalid_steam_path) echo "Это не папка Steam (внутри нет 'steamapps')." ;;
|
||
invalid_choice) echo "Неверный выбор." ;;
|
||
log_empty) echo "Лог пуст." ;;
|
||
confirm_install) echo "Продолжить? [Y/n]: " ;;
|
||
cancelled) echo "Отменено." ;;
|
||
no_dlc_to_download) echo "Все DLC актуальны." ;;
|
||
hashes_skip) echo "Не удалось получить hashes.json, проверка целостности пропущена." ;;
|
||
dlc_outdated) echo "Контент устарел, будет перекачан:" ;;
|
||
flatpak_steam) echo "(Flatpak)" ;;
|
||
*) echo "$1" ;;
|
||
esac
|
||
}
|
||
|
||
_t_zh() {
|
||
case "$1" in
|
||
title) echo "Stellaris DLC Unlocker" ;;
|
||
menu_header) echo "主菜单" ;;
|
||
m_status) echo "状态" ;;
|
||
m_install) echo "安装 / 更新" ;;
|
||
fetching_dlc_info) echo "正在获取 DLC 信息:" ;;
|
||
fetching_file) echo " -> 正在处理:" ;;
|
||
m_steam_launch) echo "设置 Steam 启动选项" ;;
|
||
m_lang) echo "语言" ;;
|
||
m_log) echo "日志" ;;
|
||
m_exit) echo "退出" ;;
|
||
choose) echo "> " ;;
|
||
steam_found) echo "Steam:" ;;
|
||
steam_not_found) echo "未找到 Steam。" ;;
|
||
game_found) echo "Stellaris:" ;;
|
||
game_not_found) echo "未找到 Stellaris。" ;;
|
||
native_build) echo "原生 Linux 版本。" ;;
|
||
not_native) echo "非原生 Linux 版本。请使用 Proton 解锁器。" ;;
|
||
downloading) echo "下载中" ;;
|
||
fetching_dlc_list) echo "正在获取 DLC 列表..." ;;
|
||
dlc_list_ok) echo "DLC 列表已加载。" ;;
|
||
dlc_list_fail) echo "获取 DLC 列表失败。" ;;
|
||
fetching_creamlinux) echo "正在下载 CreamLinux 文件..." ;;
|
||
creamlinux_ok) echo "CreamLinux 文件已就绪。" ;;
|
||
copying_files) echo "正在复制文件到游戏目录..." ;;
|
||
to_download) echo "待下载 DLC:" ;;
|
||
unpacking) echo "正在解压..." ;;
|
||
updating_ini) echo "正在更新 cream_api.ini..." ;;
|
||
ini_updated) echo "cream_api.ini: 已添加" ;;
|
||
ini_skip) echo "SteamCMD 不可用,已跳过。" ;;
|
||
install_done) echo "完成。" ;;
|
||
patch_launch_header) echo "Steam 启动选项" ;;
|
||
patch_userdata_not_found) echo "未找到 Steam userdata 目录。" ;;
|
||
patch_localconfig_not_found) echo "未找到 localconfig.vdf。" ;;
|
||
patch_backup) echo "备份:" ;;
|
||
patch_done) echo "启动选项已设置。" ;;
|
||
steam_running_prompt) echo "Steam 正在运行,需关闭后才能应用启动选项。现在关闭吗?[Y/n]: " ;;
|
||
steam_closing) echo "正在关闭 Steam..." ;;
|
||
steam_close_ok) echo "Steam 已关闭。" ;;
|
||
steam_close_fail) echo "无法关闭 Steam,已中止。" ;;
|
||
abort_steam_running) echo "已跳过:Steam 仍在运行。" ;;
|
||
press_enter) echo "按 Enter 继续..." ;;
|
||
enter_game_path) echo "游戏路径(直接 Enter 取消): " ;;
|
||
invalid_game_path) echo "路径不存在。" ;;
|
||
enter_steam_path) echo "未自动检测到 Steam。请输入 Steam 文件夹路径(直接 Enter 取消): " ;;
|
||
invalid_steam_path) echo "不是有效的 Steam 文件夹(内部没有 'steamapps')。" ;;
|
||
invalid_choice) echo "无效选择。" ;;
|
||
log_empty) echo "日志为空。" ;;
|
||
confirm_install) echo "继续?[Y/n]: " ;;
|
||
cancelled) echo "已取消。" ;;
|
||
no_dlc_to_download) echo "所有 DLC 均为最新。" ;;
|
||
hashes_skip) echo "无法获取 hashes.json,跳过完整性检查。" ;;
|
||
dlc_outdated) echo "内容已过期,将重新下载:" ;;
|
||
flatpak_steam) echo "(Flatpak)" ;;
|
||
*) echo "$1" ;;
|
||
esac
|
||
}
|
||
|
||
log() {
|
||
local level="$1"; shift
|
||
local msg="$*"
|
||
local color="$C_WHITE"
|
||
case "$level" in
|
||
OK) color="$C_GREEN" ;;
|
||
WARN) color="$C_YELLOW" ;;
|
||
ERROR) color="$C_RED" ;;
|
||
INFO) color="$C_CYAN" ;;
|
||
esac
|
||
echo -e "${color}[$level]${C_RESET} $msg"
|
||
echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $msg" >> "$LOG_FILE"
|
||
}
|
||
|
||
hr() { printf "${C_DIM}%s${C_RESET}\n" "------------------------------------------------------------"; }
|
||
|
||
header() {
|
||
clear
|
||
echo -e "${C_BOLD}${C_MAGENTA}=============================================================${C_RESET}"
|
||
echo -e "${C_BOLD}${C_WHITE} $(t title)${C_RESET}"
|
||
echo -e "${C_BOLD}${C_MAGENTA}=============================================================${C_RESET}"
|
||
echo
|
||
}
|
||
|
||
pause() {
|
||
echo
|
||
ask "$(t press_enter)" _dummy
|
||
}
|
||
|
||
progress_bar() {
|
||
local current="$1" total="$2" label="$3"
|
||
local width=30
|
||
local pct=0
|
||
[ "$total" -gt 0 ] && pct=$(( current * 100 / total ))
|
||
local filled=$(( pct * width / 100 ))
|
||
local empty=$(( width - filled ))
|
||
printf "\r${C_CYAN}[%-${width}s]${C_RESET} %3d%% %s" \
|
||
"$(printf '#%.0s' $(seq 1 $filled) 2>/dev/null)$(printf '.%.0s' $(seq 1 $empty) 2>/dev/null)" \
|
||
"$pct" "$label"
|
||
}
|
||
|
||
download_file() {
|
||
local url="$1" dest="$2" label="${3:-$1}"
|
||
mkdir -p "$(dirname "$dest")"
|
||
echo -e "${C_CYAN}$(t downloading):${C_RESET} $label"
|
||
if ! curl -L --fail --retry 3 --connect-timeout 15 -o "$dest" \
|
||
--progress-bar "$url"; then
|
||
log ERROR "Failed to download: $url"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
http_get() {
|
||
curl -sL --fail --retry 1 --connect-timeout 5 --max-time 10 -A "StellarisDLCUnlocker-sh/1.0" "$1"
|
||
}
|
||
|
||
http_get_fast() {
|
||
curl -sL --fail --connect-timeout 3 --max-time 3 -A "StellarisDLCUnlocker-sh/1.0" "$1"
|
||
}
|
||
|
||
download_with_fallback() {
|
||
local rel="$1" dest="$2" label="${3:-$1}"
|
||
mkdir -p "$(dirname "$dest")"
|
||
echo -e "${C_DIM}$(t fetching_file) ${C_WHITE}$label${C_RESET}"
|
||
if curl -fsSL --retry 2 --connect-timeout 10 -o "$dest" "$REPO_GITHUB_RAW/$rel" 2>>"$LOG_FILE"; then
|
||
log OK " (GitHub) $label"
|
||
return 0
|
||
fi
|
||
log WARN " GitHub failed for $label, trying jsDelivr..."
|
||
if curl -fsSL --retry 2 --connect-timeout 10 -o "$dest" "$REPO_JSDELIVR/$rel" 2>>"$LOG_FILE"; then
|
||
log OK " (jsDelivr) $label"
|
||
return 0
|
||
fi
|
||
log ERROR " Failed to fetch $label from GitHub and jsDelivr."
|
||
return 1
|
||
}
|
||
|
||
get_github_dir_shas() {
|
||
local subdir="$1"
|
||
local api_url="https://api.github.com/repos/seuyh/stellaris-dlc-unlocker/contents/$subdir"
|
||
http_get "$api_url" 2>/dev/null || true
|
||
}
|
||
|
||
get_manifest_shas() {
|
||
local subdir="$1"
|
||
local manifest
|
||
manifest=$(http_get "$REPO_GITHUB_RAW/$subdir/manifest.json" 2>/dev/null) || true
|
||
if [ -z "$manifest" ]; then
|
||
manifest=$(http_get "$REPO_JSDELIVR/$subdir/manifest.json" 2>/dev/null) || true
|
||
fi
|
||
echo "$manifest"
|
||
}
|
||
|
||
get_remote_sha() {
|
||
local filename="$1" api_json="$2" manifest_json="$3"
|
||
local sha=""
|
||
if command -v jq >/dev/null 2>&1; then
|
||
[ -n "$api_json" ] && sha=$(echo "$api_json" | jq -r --arg f "$filename" '.[] | select(.name==$f) | .sha // empty' 2>/dev/null)
|
||
if [ -z "$sha" ] && [ -n "$manifest_json" ]; then
|
||
sha=$(echo "$manifest_json" | jq -r --arg f "$filename" '.[$f] // empty' 2>/dev/null)
|
||
fi
|
||
else
|
||
if [ -n "$api_json" ]; then
|
||
sha=$(echo "$api_json" | grep -A5 "\"name\": \"$filename\"" | grep -oP '"sha":\s*"\K[a-f0-9]+' | head -1)
|
||
fi
|
||
if [ -z "$sha" ] && [ -n "$manifest_json" ]; then
|
||
sha=$(echo "$manifest_json" | grep -oP "\"$filename\":\\s*\"\\K[a-f0-9]+" | head -1)
|
||
fi
|
||
fi
|
||
echo "$sha"
|
||
}
|
||
|
||
download_with_cache() {
|
||
local rel="$1" dest="$2" label="${3:-$1}" remote_sha="${4:-}"
|
||
local sha_file="${dest}.sha"
|
||
if [ -n "$remote_sha" ] && [ -f "$dest" ] && [ -f "$sha_file" ]; then
|
||
local cached_sha
|
||
cached_sha=$(cat "$sha_file")
|
||
if [ "$cached_sha" = "$remote_sha" ]; then
|
||
log OK " (cached) $label"
|
||
return 0
|
||
fi
|
||
fi
|
||
download_with_fallback "$rel" "$dest" "$label" || return 1
|
||
[ -n "$remote_sha" ] && echo "$remote_sha" > "$sha_file"
|
||
return 0
|
||
}
|
||
|
||
ask() {
|
||
local prompt="$1" __var="$2" __val=""
|
||
if [ -r /dev/tty ]; then
|
||
read -rp "$prompt" __val </dev/tty
|
||
else
|
||
read -rp "$prompt" __val
|
||
fi
|
||
printf -v "$__var" '%s' "$__val"
|
||
}
|
||
|
||
STEAM_DIR=""
|
||
GAME_DIR=""
|
||
IS_FLATPAK_STEAM=0
|
||
|
||
find_steam_dir() {
|
||
local candidates=(
|
||
"$HOME/.steam/steam"
|
||
"$HOME/.local/share/Steam"
|
||
"$HOME/.var/app/com.valvesoftware.Steam/data/Steam"
|
||
"$HOME/snap/steam/common/.local/share/Steam"
|
||
)
|
||
for c in "${candidates[@]}"; do
|
||
if [ -d "$c/steamapps" ]; then
|
||
STEAM_DIR="$c"
|
||
[[ "$c" == *".var/app/com.valvesoftware.Steam"* ]] && IS_FLATPAK_STEAM=1
|
||
[[ "$c" == *"snap/steam"* ]] && IS_FLATPAK_STEAM=1
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
find_game_dir() {
|
||
[ -z "$STEAM_DIR" ] && return 1
|
||
local lib_vdf="$STEAM_DIR/steamapps/libraryfolders.vdf"
|
||
local libs=("$STEAM_DIR")
|
||
|
||
if [ -f "$lib_vdf" ]; then
|
||
while IFS= read -r p; do
|
||
[ -n "$p" ] && libs+=("$p")
|
||
done < <(grep -oP '"path"\s*"\K[^"]+' "$lib_vdf" 2>/dev/null)
|
||
fi
|
||
|
||
for lib in "${libs[@]}"; do
|
||
local candidate="$lib/steamapps/common/Stellaris"
|
||
if [ -d "$candidate" ]; then
|
||
GAME_DIR="$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
is_native_build() {
|
||
[ -f "$GAME_DIR/stellaris" ] && file "$GAME_DIR/stellaris" 2>/dev/null | grep -qi "ELF"
|
||
}
|
||
|
||
resolve_steam_dir() {
|
||
if [ -n "$STEAM_DIR" ]; then
|
||
return 0
|
||
fi
|
||
if find_steam_dir; then
|
||
return 0
|
||
fi
|
||
while true; do
|
||
ask "$(t enter_steam_path)" manual
|
||
[ -z "$manual" ] && return 1
|
||
manual="${manual%/}"
|
||
if [ -d "$manual/steamapps" ]; then
|
||
STEAM_DIR="$manual"
|
||
[[ "$manual" == *".var/app/com.valvesoftware.Steam"* || "$manual" == *"flatpak"* || "$manual" == *"snap/steam"* ]] && IS_FLATPAK_STEAM=1
|
||
return 0
|
||
fi
|
||
log ERROR "$(t invalid_steam_path)"
|
||
done
|
||
}
|
||
|
||
resolve_game_dir() {
|
||
if find_game_dir; then
|
||
return 0
|
||
fi
|
||
while true; do
|
||
ask "$(t enter_game_path)" manual
|
||
[ -z "$manual" ] && return 1
|
||
manual="${manual%/}"
|
||
if [ -d "$manual" ]; then
|
||
GAME_DIR="$manual"
|
||
return 0
|
||
fi
|
||
log ERROR "$(t invalid_game_path)"
|
||
done
|
||
}
|
||
|
||
is_steam_running() {
|
||
pgrep -x steam >/dev/null 2>&1 && return 0
|
||
pgrep -f "com.valvesoftware.Steam" >/dev/null 2>&1 && return 0
|
||
return 1
|
||
}
|
||
|
||
close_steam() {
|
||
log INFO "$(t steam_closing)"
|
||
if [ "$IS_FLATPAK_STEAM" = 1 ] && command -v flatpak >/dev/null 2>&1; then
|
||
flatpak kill com.valvesoftware.Steam >/dev/null 2>&1
|
||
fi
|
||
command -v steam >/dev/null 2>&1 && steam -shutdown >/dev/null 2>&1 &
|
||
pkill -x steam >/dev/null 2>&1
|
||
|
||
local waited=0
|
||
while is_steam_running && [ "$waited" -lt 15 ]; do
|
||
sleep 1
|
||
waited=$((waited+1))
|
||
done
|
||
|
||
if is_steam_running; then
|
||
pkill -9 -x steam >/dev/null 2>&1
|
||
sleep 1
|
||
fi
|
||
|
||
if is_steam_running; then
|
||
log ERROR "$(t steam_close_fail)"
|
||
return 1
|
||
fi
|
||
log OK "$(t steam_close_ok)"
|
||
return 0
|
||
}
|
||
|
||
ensure_steam_closed() {
|
||
if ! is_steam_running; then
|
||
return 0
|
||
fi
|
||
ask "$(t steam_running_prompt)" ans
|
||
[ -z "$ans" ] && ans="y"
|
||
if [[ "$ans" =~ ^[yYдД]$ ]]; then
|
||
close_steam || return 1
|
||
return 0
|
||
fi
|
||
log WARN "$(t abort_steam_running)"
|
||
return 1
|
||
}
|
||
|
||
DLC_DATA_JSON=""
|
||
|
||
fetch_dlc_data() {
|
||
log INFO "$(t fetching_dlc_list)"
|
||
DLC_DATA_JSON=$(http_get "$GITHUB_DLC_URL" 2>/dev/null) || true
|
||
if [ -z "$DLC_DATA_JSON" ]; then
|
||
log WARN "GitHub failed, trying jsDelivr..."
|
||
DLC_DATA_JSON=$(http_get "$JSDELIVR_DLC_URL" 2>/dev/null) || true
|
||
fi
|
||
if [ -z "$DLC_DATA_JSON" ]; then
|
||
log ERROR "$(t dlc_list_fail)"
|
||
return 1
|
||
fi
|
||
log OK "$(t dlc_list_ok)"
|
||
return 0
|
||
}
|
||
|
||
dlc_folders() {
|
||
if command -v jq >/dev/null 2>&1; then
|
||
echo "$DLC_DATA_JSON" | jq -r '.[].dlc_folder // empty'
|
||
else
|
||
echo "$DLC_DATA_JSON" | grep -oP '"dlc_folder"\s*:\s*"\K[^"]+'
|
||
fi
|
||
}
|
||
|
||
fetch_creamlinux() {
|
||
local dest_dir="$CACHE_DIR/$CREAMLINUX_SUBDIR"
|
||
mkdir -p "$dest_dir"
|
||
log INFO "$(t fetching_creamlinux)"
|
||
|
||
local api_json manifest_json
|
||
api_json=$(get_github_dir_shas "$CREAMLINUX_SUBDIR")
|
||
manifest_json=$(get_manifest_shas "$CREAMLINUX_SUBDIR")
|
||
|
||
local ok=1
|
||
for f in "${CREAMLINUX_FILES[@]}"; do
|
||
local remote_sha
|
||
remote_sha=$(get_remote_sha "$f" "$api_json" "$manifest_json")
|
||
download_with_cache "$CREAMLINUX_SUBDIR/$f" "$dest_dir/$f" "$f" "$remote_sha" || ok=0
|
||
done
|
||
|
||
if [ "$ok" -eq 1 ]; then
|
||
log OK "$(t creamlinux_ok)"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
copy_creamlinux_to_game() {
|
||
local src_dir="$CACHE_DIR/$CREAMLINUX_SUBDIR"
|
||
log INFO "$(t copying_files)"
|
||
for f in cream.sh lib32Creamlinux.so lib64Creamlinux.so; do
|
||
if [ -f "$src_dir/$f" ]; then
|
||
cp -f "$src_dir/$f" "$GAME_DIR/$f"
|
||
else
|
||
log WARN "Missing in CreamLinux package: $f"
|
||
fi
|
||
done
|
||
chmod +x "$GAME_DIR/cream.sh" 2>/dev/null
|
||
log OK "CreamLinux files copied to: $GAME_DIR"
|
||
}
|
||
|
||
update_cream_ini() {
|
||
local ini_path="$GAME_DIR/cream_api.ini"
|
||
local cached_ini="$CACHE_DIR/$CREAMLINUX_SUBDIR/cream_api.ini"
|
||
|
||
if [ -f "$cached_ini" ]; then
|
||
cp -f "$cached_ini" "$ini_path"
|
||
sed -i 's/\r$//' "$ini_path"
|
||
else
|
||
: > "$ini_path"
|
||
fi
|
||
|
||
log INFO "$(t updating_ini)"
|
||
local info
|
||
info=$(http_get "$STEAMCMD_API/$APPID" 2>/dev/null) || true
|
||
if [ -z "$info" ]; then
|
||
log WARN "$(t ini_skip)"
|
||
return 0
|
||
fi
|
||
|
||
local csv
|
||
if command -v jq >/dev/null 2>&1; then
|
||
csv=$(echo "$info" | jq -r ".data[\"$APPID\"].extended.listofdlc // empty")
|
||
else
|
||
csv=$(echo "$info" | grep -oP '"listofdlc"\s*:\s*"\K[^"]+')
|
||
fi
|
||
if [ -z "$csv" ]; then
|
||
log WARN "$(t ini_skip)"
|
||
return 0
|
||
fi
|
||
|
||
if [ -s "$ini_path" ] && [ -n "$(tail -c1 "$ini_path")" ]; then
|
||
printf '\n' >> "$ini_path"
|
||
fi
|
||
|
||
local added=0
|
||
local pending=()
|
||
IFS=',' read -ra ids <<< "$csv"
|
||
for id in "${ids[@]}"; do
|
||
id="$(echo "$id" | xargs)"
|
||
[ -z "$id" ] && continue
|
||
grep -q "^$id " "$ini_path" 2>/dev/null && continue
|
||
pending+=("$id")
|
||
done
|
||
|
||
local total=${#pending[@]}
|
||
local current=0
|
||
for id in "${pending[@]}"; do
|
||
current=$((current+1))
|
||
printf "\r${C_DIM}$(t fetching_dlc_info) [%d/%d] %s...${C_RESET} " "$current" "$total" "$id"
|
||
local name="$id"
|
||
local dlc_info
|
||
dlc_info=$(http_get_fast "$STEAMCMD_API/$id" 2>/dev/null) || true
|
||
if [ -n "$dlc_info" ]; then
|
||
if command -v jq >/dev/null 2>&1; then
|
||
local n
|
||
n=$(echo "$dlc_info" | jq -r ".data[\"$id\"].common.name // empty")
|
||
[ -n "$n" ] && name="$n"
|
||
else
|
||
local n
|
||
n=$(echo "$dlc_info" | grep -oP '"name"\s*:\s*"\K[^"]+' | head -1)
|
||
[ -n "$n" ] && name="$n"
|
||
fi
|
||
fi
|
||
echo "$id = $name" >> "$ini_path"
|
||
added=$((added+1))
|
||
done
|
||
|
||
[ "$added" -gt 0 ] && echo ""
|
||
|
||
if [ "$added" -gt 0 ]; then
|
||
log OK "$(t ini_updated) +$added"
|
||
fi
|
||
}
|
||
|
||
DLC_HASHES_JSON=""
|
||
|
||
fetch_dlc_hashes() {
|
||
DLC_HASHES_JSON=$(http_get "$GITHUB_HASHES_URL" 2>/dev/null) || true
|
||
if [ -z "$DLC_HASHES_JSON" ]; then
|
||
DLC_HASHES_JSON=$(http_get "$JSDELIVR_HASHES_URL" 2>/dev/null) || true
|
||
fi
|
||
[ -n "$DLC_HASHES_JSON" ]
|
||
}
|
||
|
||
dlc_hash_entries() {
|
||
if command -v jq >/dev/null 2>&1; then
|
||
echo "$DLC_HASHES_JSON" | jq -r 'to_entries[] | "\(.key)\t\(.value)"'
|
||
else
|
||
echo "$DLC_HASHES_JSON" | grep -oP '"[^"]+"\s*:\s*"[a-fA-F0-9]{32}"' \
|
||
| sed -E 's/^"([^"]+)"[[:space:]]*:[[:space:]]*"([a-fA-F0-9]{32})"$/\1\t\2/'
|
||
fi
|
||
}
|
||
|
||
mark_stale_dlc_folders() {
|
||
local dlc_dir="$1"
|
||
fetch_dlc_hashes || { log WARN "$(t hashes_skip)"; return; }
|
||
|
||
declare -A expected_hash=()
|
||
while IFS=$'\t' read -r relpath md5; do
|
||
[ -n "$relpath" ] && expected_hash["$relpath"]="$md5"
|
||
done < <(dlc_hash_entries)
|
||
|
||
[ ${#expected_hash[@]} -eq 0 ] && return
|
||
|
||
local stale_folders=()
|
||
local key
|
||
for key in "${!expected_hash[@]}"; do
|
||
local folder="${key%%/*}"
|
||
local already=0
|
||
local sf
|
||
for sf in "${stale_folders[@]:-}"; do
|
||
[ "$sf" = "$folder" ] && already=1 && break
|
||
done
|
||
[ "$already" -eq 1 ] && continue
|
||
[ -d "$dlc_dir/$folder" ] || continue
|
||
|
||
local localfile="$dlc_dir/$key"
|
||
if [ ! -f "$localfile" ]; then
|
||
stale_folders+=("$folder")
|
||
continue
|
||
fi
|
||
local actual
|
||
actual=$(md5sum "$localfile" 2>/dev/null | awk '{print $1}')
|
||
if [ "$actual" != "${expected_hash[$key]}" ]; then
|
||
stale_folders+=("$folder")
|
||
fi
|
||
done
|
||
|
||
for sf in "${stale_folders[@]}"; do
|
||
log WARN "$(t dlc_outdated) $sf"
|
||
rm -rf "$dlc_dir/$sf"
|
||
done
|
||
}
|
||
|
||
download_dlc_content() {
|
||
local dlc_dir="$GAME_DIR/dlc"
|
||
mkdir -p "$dlc_dir"
|
||
|
||
mark_stale_dlc_folders "$dlc_dir"
|
||
|
||
local folders=()
|
||
while IFS= read -r f; do
|
||
[ -n "$f" ] && folders+=("$f")
|
||
done < <(dlc_folders)
|
||
|
||
local total=${#folders[@]}
|
||
local queue=()
|
||
for f in "${folders[@]}"; do
|
||
local dir="$dlc_dir/$f"
|
||
local zip="$dlc_dir/$f.zip"
|
||
if [ -d "$dir" ]; then continue; fi
|
||
if [ -f "$zip" ]; then
|
||
unzip -tq "$zip" >/dev/null 2>&1 && continue
|
||
rm -f "$zip"
|
||
fi
|
||
queue+=("$f")
|
||
done
|
||
|
||
if [ ${#queue[@]} -eq 0 ]; then
|
||
log OK "$(t no_dlc_to_download)"
|
||
return 0
|
||
fi
|
||
|
||
log INFO "$(t to_download) ${#queue[@]} / $total"
|
||
local i=0
|
||
for f in "${queue[@]}"; do
|
||
i=$((i+1))
|
||
local url="https://$SERVER_URL/unlocker/$f.zip"
|
||
local dest="$dlc_dir/$f.zip"
|
||
echo
|
||
log INFO "[$i/${#queue[@]}] $f"
|
||
download_file "$url" "$dest" "$f" || continue
|
||
done
|
||
|
||
log INFO "$(t unpacking)"
|
||
for z in "$dlc_dir"/*.zip; do
|
||
[ -f "$z" ] || continue
|
||
local base
|
||
base="$(basename "$z" .zip)"
|
||
if unzip -oq "$z" -d "$dlc_dir"; then
|
||
rm -f "$z"
|
||
log OK "Unpacked: $base"
|
||
else
|
||
log ERROR "Unzip error: $base"
|
||
fi
|
||
done
|
||
}
|
||
|
||
patch_launch_options_core() {
|
||
resolve_steam_dir || { log ERROR "$(t steam_not_found)"; return 1; }
|
||
|
||
ensure_steam_closed || return 1
|
||
|
||
local userdata_dir="$STEAM_DIR/userdata"
|
||
if [ ! -d "$userdata_dir" ]; then
|
||
log ERROR "$(t patch_userdata_not_found)"
|
||
return 1
|
||
fi
|
||
|
||
local patched_any=0
|
||
for user_dir in "$userdata_dir"/*/; do
|
||
local vdf="${user_dir}config/localconfig.vdf"
|
||
[ -f "$vdf" ] || continue
|
||
|
||
local backup="${vdf}.bak.$(date +%Y%m%d%H%M%S)"
|
||
cp -f "$vdf" "$backup"
|
||
log INFO "$(t patch_backup) $backup"
|
||
|
||
local desired='sh ./cream.sh %command%'
|
||
local tmp
|
||
tmp=$(mktemp)
|
||
|
||
awk -v appid="\"$APPID\"" -v opt="$desired" '
|
||
BEGIN { in_block=0; depth=0; done=0 }
|
||
{
|
||
line=$0
|
||
trimmed=line
|
||
gsub(/^[ \t]+|[ \t]+$/, "", trimmed)
|
||
|
||
if (!in_block && trimmed == appid) {
|
||
in_block=1
|
||
print line
|
||
next
|
||
}
|
||
|
||
if (in_block && depth==0 && trimmed == "{") {
|
||
depth=1
|
||
print line
|
||
next
|
||
}
|
||
|
||
if (in_block && depth==1) {
|
||
if (trimmed ~ /^"LaunchOptions"/) {
|
||
sub(/"LaunchOptions".*/, "\"LaunchOptions\"\t\t\"" opt "\"", line)
|
||
print line
|
||
done=1
|
||
next
|
||
}
|
||
if (trimmed == "{") { depth++; print line; next }
|
||
if (trimmed == "}") {
|
||
if (!done) {
|
||
print "\t\t\t\"LaunchOptions\"\t\t\"" opt "\""
|
||
done=1
|
||
}
|
||
in_block=0
|
||
depth=0
|
||
print line
|
||
next
|
||
}
|
||
}
|
||
|
||
if (in_block && depth>1) {
|
||
if (trimmed == "{") depth++
|
||
if (trimmed == "}") depth--
|
||
}
|
||
|
||
print line
|
||
}
|
||
' "$vdf" > "$tmp"
|
||
|
||
if grep -q "$APPID" "$tmp"; then
|
||
mv "$tmp" "$vdf"
|
||
patched_any=1
|
||
log OK "$(t patch_done)"
|
||
else
|
||
rm -f "$tmp"
|
||
log WARN "AppID $APPID not found in $vdf"
|
||
fi
|
||
done
|
||
|
||
if [ "$patched_any" -eq 0 ]; then
|
||
log ERROR "$(t patch_localconfig_not_found)"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
patch_launch_options() {
|
||
header
|
||
echo -e "${C_BOLD}$(t patch_launch_header)${C_RESET}"
|
||
hr
|
||
patch_launch_options_core
|
||
pause
|
||
}
|
||
|
||
do_install() {
|
||
header
|
||
echo -e "${C_BOLD}$(t m_install)${C_RESET}"
|
||
hr
|
||
|
||
resolve_steam_dir
|
||
if [ -z "$STEAM_DIR" ]; then
|
||
log ERROR "$(t steam_not_found)"
|
||
pause; return 1
|
||
fi
|
||
log OK "$(t steam_found) $STEAM_DIR $([ "$IS_FLATPAK_STEAM" = 1 ] && t flatpak_steam)"
|
||
|
||
resolve_game_dir
|
||
if [ -z "$GAME_DIR" ]; then
|
||
log ERROR "$(t game_not_found)"
|
||
pause; return 1
|
||
fi
|
||
log OK "$(t game_found) $GAME_DIR"
|
||
|
||
if is_native_build; then
|
||
log OK "$(t native_build)"
|
||
else
|
||
log ERROR "$(t not_native)"
|
||
pause; return 1
|
||
fi
|
||
|
||
echo
|
||
ask "$(t confirm_install)" confirm
|
||
[ -z "$confirm" ] && confirm="y"
|
||
if [[ ! "$confirm" =~ ^[yYдД]$ ]]; then
|
||
log INFO "$(t cancelled)"
|
||
pause; return 0
|
||
fi
|
||
|
||
fetch_dlc_data || { pause; return 1; }
|
||
fetch_creamlinux || { pause; return 1; }
|
||
copy_creamlinux_to_game
|
||
mkdir -p "$GAME_DIR/dlc"
|
||
update_cream_ini
|
||
download_dlc_content
|
||
|
||
echo
|
||
echo -e "${C_BOLD}$(t patch_launch_header)${C_RESET}"
|
||
patch_launch_options_core
|
||
|
||
echo
|
||
log OK "$(t install_done)"
|
||
pause
|
||
}
|
||
|
||
show_status() {
|
||
header
|
||
echo -e "${C_BOLD}$(t m_status)${C_RESET}"
|
||
hr
|
||
|
||
find_steam_dir
|
||
if [ -n "$STEAM_DIR" ]; then
|
||
echo -e "${C_GREEN}$(t steam_found)${C_RESET} $STEAM_DIR $([ "$IS_FLATPAK_STEAM" = 1 ] && t flatpak_steam)"
|
||
else
|
||
echo -e "${C_RED}$(t steam_not_found)${C_RESET}"
|
||
fi
|
||
|
||
find_game_dir
|
||
if [ -n "$GAME_DIR" ]; then
|
||
echo -e "${C_GREEN}$(t game_found)${C_RESET} $GAME_DIR"
|
||
if is_native_build; then
|
||
echo -e "${C_GREEN}$(t native_build)${C_RESET}"
|
||
else
|
||
echo -e "${C_RED}$(t not_native)${C_RESET}"
|
||
fi
|
||
if [ -f "$GAME_DIR/cream.sh" ]; then
|
||
echo -e "${C_GREEN}cream.sh:${C_RESET} installed"
|
||
else
|
||
echo -e "${C_YELLOW}cream.sh:${C_RESET} not installed"
|
||
fi
|
||
if [ -f "$GAME_DIR/cream_api.ini" ]; then
|
||
local cnt
|
||
cnt=$(grep -c "=" "$GAME_DIR/cream_api.ini" 2>/dev/null || echo 0)
|
||
echo -e "${C_GREEN}cream_api.ini:${C_RESET} $cnt DLC entries"
|
||
fi
|
||
else
|
||
echo -e "${C_RED}$(t game_not_found)${C_RESET}"
|
||
fi
|
||
pause
|
||
}
|
||
|
||
show_log() {
|
||
header
|
||
echo -e "${C_BOLD}$(t m_log)${C_RESET}"
|
||
hr
|
||
if [ -s "$LOG_FILE" ]; then
|
||
tail -n 50 "$LOG_FILE"
|
||
else
|
||
echo "$(t log_empty)"
|
||
fi
|
||
pause
|
||
}
|
||
|
||
change_language() {
|
||
header
|
||
echo "1) English"
|
||
echo "2) Русский"
|
||
echo "3) 中文"
|
||
ask "$(t choose)" l
|
||
case "$l" in
|
||
1) LANGUAGE="en" ;;
|
||
2) LANGUAGE="ru" ;;
|
||
3) LANGUAGE="zh" ;;
|
||
esac
|
||
echo "$LANGUAGE" > "$CONFIG_FILE"
|
||
log OK "$LANGUAGE"
|
||
pause
|
||
}
|
||
|
||
check_deps() {
|
||
local missing=()
|
||
for c in curl unzip grep awk; do
|
||
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
|
||
done
|
||
if [ ${#missing[@]} -gt 0 ]; then
|
||
echo -e "${C_RED}Missing required tools: ${missing[*]}${C_RESET}"
|
||
echo "Install them with your package manager, e.g.: sudo apt install ${missing[*]}"
|
||
exit 1
|
||
fi
|
||
command -v jq >/dev/null 2>&1 || log WARN "jq not found — falling back to grep/sed JSON parsing (less robust). Install jq for best reliability."
|
||
}
|
||
|
||
main_menu() {
|
||
while true; do
|
||
header
|
||
echo -e "${C_BOLD}$(t menu_header)${C_RESET}"
|
||
hr
|
||
echo -e " ${C_CYAN}1)${C_RESET} $(t m_status)"
|
||
echo -e " ${C_CYAN}2)${C_RESET} $(t m_install)"
|
||
echo -e " ${C_CYAN}3)${C_RESET} $(t m_steam_launch)"
|
||
echo -e " ${C_CYAN}4)${C_RESET} Language / Язык / 中文"
|
||
echo -e " ${C_CYAN}5)${C_RESET} $(t m_log)"
|
||
echo -e " ${C_CYAN}0)${C_RESET} $(t m_exit)"
|
||
hr
|
||
ask "$(t choose)" choice
|
||
case "$choice" in
|
||
1) show_status ;;
|
||
2) do_install ;;
|
||
3) patch_launch_options ;;
|
||
4) change_language ;;
|
||
5) show_log ;;
|
||
0) exit 0 ;;
|
||
*) log WARN "$(t invalid_choice)"; sleep 1 ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
detect_system_lang() {
|
||
local loc="${LANG:-en}"
|
||
loc="$(echo "$loc" | tr '[:upper:]' '[:lower:]')"
|
||
case "$loc" in
|
||
ru*) echo ru ;;
|
||
zh*) echo zh ;;
|
||
*) echo en ;;
|
||
esac
|
||
}
|
||
|
||
if [ -f "$CONFIG_FILE" ]; then
|
||
LANGUAGE="$(cat "$CONFIG_FILE")"
|
||
else
|
||
LANGUAGE="$(detect_system_lang)"
|
||
fi
|
||
check_deps
|
||
main_menu
|