From 0d6aba7e45f82dcdc8b8a1ed0446582df26df258 Mon Sep 17 00:00:00 2001 From: seuyh Date: Tue, 30 Jun 2026 11:57:29 +0700 Subject: [PATCH] Update --- generate_manifests.py => GenerateManifests.py | 0 Libs/ConnectionCheck.py | 33 - Libs/CreamApiMaker.py | 97 -- Libs/DownloadThread.py | 63 - Libs/GamePath.py | 99 -- Libs/LauncherReinstall.py | 137 -- Libs/MD5Check.py | 58 - Libs/ServerData.py | 57 - Libs/logger.py | 76 - README.md | 78 - README_EN.md | 63 - README_ZHCN.md | 63 - StellarisDLCUnlocker.ps1 | 459 ++++-- StellarisDLCUnlocker.sh | 749 +++++++++ StellarisDLCUnlocker2.ps1 | 1067 ------------ UI/icons/stellaris.png | Bin 17048 -> 0 bytes UI/recources_rc.py | 1004 ------------ UI/translations/ru_RU.qm | Bin 22095 -> 0 bytes UI/translations/ru_RU.ts | 336 ---- UI/translations/zh_CN.qm | Bin 19387 -> 0 bytes UI/translations/zh_CN.ts | 334 ---- UI/ui_dialog.py | 167 -- UI/ui_error.py | 99 -- UI/ui_main.py | 1433 ----------------- UI_logic/DialogWindow.py | 57 - UI_logic/ErrorWindow.py | 57 - UI_logic/MainWindow.py | 854 ---------- creamlinux/cream_api.ini | 39 + creamlinux/lib32Creamlinux.so | Bin 0 -> 3301008 bytes creamlinux/lib64Creamlinux.so | Bin 0 -> 3655512 bytes data.json | 5 - main.py | 15 - requirements.txt | 4 - 33 files changed, 1157 insertions(+), 6346 deletions(-) rename generate_manifests.py => GenerateManifests.py (100%) delete mode 100644 Libs/ConnectionCheck.py delete mode 100644 Libs/CreamApiMaker.py delete mode 100644 Libs/DownloadThread.py delete mode 100644 Libs/GamePath.py delete mode 100644 Libs/LauncherReinstall.py delete mode 100644 Libs/MD5Check.py delete mode 100644 Libs/ServerData.py delete mode 100644 Libs/logger.py create mode 100644 StellarisDLCUnlocker.sh delete mode 100644 StellarisDLCUnlocker2.ps1 delete mode 100644 UI/icons/stellaris.png delete mode 100644 UI/recources_rc.py delete mode 100644 UI/translations/ru_RU.qm delete mode 100644 UI/translations/ru_RU.ts delete mode 100644 UI/translations/zh_CN.qm delete mode 100644 UI/translations/zh_CN.ts delete mode 100644 UI/ui_dialog.py delete mode 100644 UI/ui_error.py delete mode 100644 UI/ui_main.py delete mode 100644 UI_logic/DialogWindow.py delete mode 100644 UI_logic/ErrorWindow.py delete mode 100644 UI_logic/MainWindow.py create mode 100644 creamlinux/cream_api.ini create mode 100644 creamlinux/lib32Creamlinux.so create mode 100644 creamlinux/lib64Creamlinux.so delete mode 100644 data.json delete mode 100644 main.py delete mode 100644 requirements.txt diff --git a/generate_manifests.py b/GenerateManifests.py similarity index 100% rename from generate_manifests.py rename to GenerateManifests.py diff --git a/Libs/ConnectionCheck.py b/Libs/ConnectionCheck.py deleted file mode 100644 index 9b8edcb..0000000 --- a/Libs/ConnectionCheck.py +++ /dev/null @@ -1,33 +0,0 @@ -import requests -from PyQt5.QtCore import QThread, pyqtSignal - -class ConnectionCheckThread(QThread): - github_status_checked = pyqtSignal(bool) - server_status_checked = pyqtSignal(bool) - - def __init__(self, server_url, github_api_url): - super().__init__() - self.server_url = server_url - self.github_api_url = github_api_url - - def run(self): - try: - response = requests.get(self.github_api_url, timeout=5) - if response.status_code == 200: - self.github_status_checked.emit(True) - else: - self.github_status_checked.emit(False) - except: - self.github_status_checked.emit(False) - - try: - if self.server_url: - response = requests.get(f'https://{self.server_url}', timeout=10) - if response.status_code == 200: - self.server_status_checked.emit(True) - else: - self.server_status_checked.emit(False) - else: - self.server_status_checked.emit(False) - except: - self.server_status_checked.emit(False) \ No newline at end of file diff --git a/Libs/CreamApiMaker.py b/Libs/CreamApiMaker.py deleted file mode 100644 index a5fbcec..0000000 --- a/Libs/CreamApiMaker.py +++ /dev/null @@ -1,97 +0,0 @@ -from idlelib.iomenu import errors - -from requests import get -from time import sleep -from PyQt5 import QtCore -import os - - -class CreamAPI(QtCore.QThread): - progress_signal = QtCore.pyqtSignal(int) - - def __init__(self): - super().__init__() - # self.dlc_callback = dlc_callback - # self.progress_callback = progress_callback - self.parent_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - def get_dlc_name(self, dlc_id, errors=0): - print('CreamApi creating...') - url = f"https://api.steamcmd.net/v1/info/{dlc_id}" - headers = { - "User-Agent": "Mozilla/5.0", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9", - "Cache-Control": "max-age=0", - "Connection": "keep-alive", - "Host": "api.steamcmd.net", - "Upgrade-Insecure-Requests": "1" - } - try: - response = get(url, headers=headers, timeout=3) - print(response) - if response.status_code == 200: - data = response.json() - dlc_name = data['data'][str(dlc_id)]['common']['name'] - print(dlc_name) - return dlc_name - else: - return None - except Exception: - if errors >= 3: - return False - errors += 1 - print('Cant connect steamcmd. Rertying...') - return self.get_dlc_name(dlc_id) - - def get_dlc_list(self, app_id, errors=0): - url = f"https://api.steamcmd.net/v1/info/{app_id}" - headers = { - "User-Agent": "Mozilla/5.0", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9", - "Cache-Control": "max-age=0", - "Connection": "keep-alive", - "Host": "api.steamcmd.net", - "Upgrade-Insecure-Requests": "1" - } - try: - response = get(url, headers=headers, timeout=8) - data = response.json() - dlc_list_json = data['data'][str(app_id)]['extended']['listofdlc'] - dlc_list = dlc_list_json.split(',') - return dlc_list - except Exception: - if errors >= 3: - return False - errors += 1 - print('Cant connect steamcmd. Rertying...') - return self.get_dlc_list(app_id, errors) - - def run(self): - print('Cream api creating...') - dlc_list = self.get_dlc_list(281990) - print(f"DLC list for Cream api {dlc_list}") - if dlc_list: - self.check_and_update_dlc_list(dlc_list, - os.path.join(self.parent_directory, 'creamapi_steam_files', 'cream_api.ini')) - self.check_and_update_dlc_list(dlc_list, - os.path.join(self.parent_directory, 'creamapi_launcher_files', 'cream_api.ini')) - # self.launcher_creamapi(dlcs) - self.progress_signal.emit(100) - else: - print('SteamCmd unavailable. Skipped') - self.progress_signal.emit(100) - return - - - def check_and_update_dlc_list(self, dlc_list, path): - with open(path, 'r+') as file: - content = file.read() - for dlc_id in dlc_list: - if str(dlc_id) not in content: - dlc_name = self.get_dlc_name(dlc_id) - file.write(f"\n{dlc_id} = {dlc_name}") - print('CreamApi writed') \ No newline at end of file diff --git a/Libs/DownloadThread.py b/Libs/DownloadThread.py deleted file mode 100644 index c3d884c..0000000 --- a/Libs/DownloadThread.py +++ /dev/null @@ -1,63 +0,0 @@ -import urllib.request -from os import remove, path -from PyQt5 import QtCore -from time import time - - -class DownloaderThread(QtCore.QThread): - progress_signal = QtCore.pyqtSignal(int, bool) - progress_signal_2 = QtCore.pyqtSignal(int) - # text_signal = QtCore.pyqtSignal(str) - error_signal = QtCore.pyqtSignal(Exception) - speed_signal = QtCore.pyqtSignal(float) - finished = QtCore.pyqtSignal() - - def __init__(self, file_url, save_path, dlc_downloaded, dlc_count): - super().__init__() - self.file_url = file_url - self.save_path = save_path - self.cancelled = False - self.downloaded_bytes = 0 - self.dlc_downloaded = dlc_downloaded - self.dlc_count = dlc_count - - def run(self): - request = urllib.request.Request(self.file_url, headers={"User-Agent": "Mozilla/5.0"}) - request.add_header('Range', f'bytes={self.downloaded_bytes}-') - try: - with urllib.request.urlopen(request, timeout=25) as response: - total_size = int(response.headers.get('content-length', 0)) + self.downloaded_bytes - start_time = time() - - with open(self.save_path, 'ab') as file: - while True: - if self.cancelled: - remove(self.save_path) - return - data = response.read(1024) - if not data: - break - file.write(data) - self.downloaded_bytes += len(data) - elapsed_time = time() - start_time - if elapsed_time > 0: - speed = round(self.downloaded_bytes / (1024 * 1024 * elapsed_time), 1) - else: - speed = 0 - progress_percentage_2 = int((self.downloaded_bytes / total_size) * 100) - self.speed_signal.emit(speed) - self.progress_signal_2.emit(progress_percentage_2) - - file.close() - - except Exception as e: - self.cancelled = True - self.error_signal.emit(e) - finally: - progress_percentage = int((self.dlc_downloaded / self.dlc_count) * 100) - self.progress_signal.emit(progress_percentage, True) - # self.text_signal.emit(path.basename(self.save_path)) - self.finished.emit() - - def cancel(self): - self.cancelled = True diff --git a/Libs/GamePath.py b/Libs/GamePath.py deleted file mode 100644 index 43661d3..0000000 --- a/Libs/GamePath.py +++ /dev/null @@ -1,99 +0,0 @@ -import os.path -import winreg -from vdf import loads -import subprocess - - -def get_powershell_path(): - candidates = [ - r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", - r"C:\Program Files\PowerShell\7\pwsh.exe" - ] - for path in candidates: - if os.path.exists(path): - return path - return None - -def get_user_logon_name(): - ps = "(Get-CimInstance -ClassName Win32_ComputerSystem).Username" - powershell = get_powershell_path() - if not powershell: - raise RuntimeError("PowerShell not found on this system") - - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = subprocess.SW_HIDE - - res = subprocess.check_output( - [powershell, "-NoProfile", "-Command", ps], - universal_newlines=True, - startupinfo=startupinfo - ).strip() - - if "\\" in res: - res = res.rsplit("\\", 1)[1] - return res - - -def get_steam_path(): - return reg_search(r"Software\Valve\Steam", "SteamPath") - - -def stellaris_path(): - try: - vdf_file_path = os.path.join(get_steam_path(), "steamapps", "libraryfolders.vdf") - with open(vdf_file_path, 'r', encoding='utf-8') as vdf_file: - vdf_data = loads(vdf_file.read()) - - if "libraryfolders" in vdf_data: - libraryfolders = vdf_data["libraryfolders"] - for key, value in libraryfolders.items(): - if "apps" in value and "281990" in value["apps"]: - return os.path.join(value["path"], "steamapps", "common", "Stellaris") - else: - return 0 - else: - return 0 - except Exception: - return 0 - - -def is_drive_root(path: str) -> bool: - if not path: - return False - norm = os.path.normpath(path) - drive, tail = os.path.splitdrive(norm) - return bool(drive) and (tail in ("", os.sep)) - - -def launcher_path(): - try: - user_logon_name = get_user_logon_name() - except: - user_logon_name = os.getlogin() - - user_home = os.path.join("C:\\Users", user_logon_name) - - launcher_path_1 = reg_search(r"Software\Paradox Interactive\Paradox Launcher v2", "LauncherInstallation") - launcher_path_2 = reg_search(r"Software\Paradox Interactive\Paradox Launcher v2", "LauncherPathFolder") - - if (launcher_path_1 and is_drive_root(launcher_path_1)) or not launcher_path_1: - launcher_path_1 = os.path.join(user_home, "AppData", "Local", "Programs", "Paradox Interactive", "launcher") - if (launcher_path_2 and is_drive_root(launcher_path_2)) or not launcher_path_2: - launcher_path_2 = os.path.join(user_home, "AppData", "Local", "Paradox Interactive") - - - launcher_path_3 = os.path.join(user_home, "AppData", "Roaming", "Paradox Interactive") - launcher_path_4 = os.path.join(user_home, "AppData", "Roaming", "paradox-launcher-v2") - - return launcher_path_1, launcher_path_2, launcher_path_3, launcher_path_4 - - -def reg_search(vaule_data, vaule_name): - try: - key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, vaule_data) - launcher_path, _ = winreg.QueryValueEx(key, vaule_name) - winreg.CloseKey(key) - return launcher_path - except: - return 0 diff --git a/Libs/LauncherReinstall.py b/Libs/LauncherReinstall.py deleted file mode 100644 index 23e0758..0000000 --- a/Libs/LauncherReinstall.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -from shutil import rmtree, move -from subprocess import Popen -from PyQt5 import QtCore -from time import sleep -from glob import glob -from re import search -from pathlib import Path - - -class ReinstallThread(QtCore.QThread): - # progress_signal = QtCore.pyqtSignal(int) - error_signal = QtCore.pyqtSignal(Exception) - continue_reinstall = QtCore.pyqtSignal(str) - - def __init__(self, msi_path, paradox_folder1, paradox_folder2, paradox_folder3, paradox_folder4, - launcher_downloaded, downloaded_launcher_dir, user_logon_name): - super().__init__() - self.msi_path = msi_path - self.user_logon_name = user_logon_name - self.paradox_folder1 = paradox_folder1 - self.paradox_folder2 = paradox_folder2 - self.paradox_folder3 = paradox_folder3 - self.paradox_folder4 = paradox_folder4 - self.launcher_downloaded = launcher_downloaded - self.downloaded_launcher_dir = downloaded_launcher_dir - - def run(self): - user_home = os.path.join("C:\\Users", self.user_logon_name) - if Path(self.paradox_folder1) == Path(self.msi_path): - self.paradox_folder1 = os.path.join(user_home, "AppData", "Local", "Programs", "Paradox Interactive", - "launcher") - latest_file = None - latest_version = (0, 0) - - def extract_version(filename): - match = search(r'launcher-installer-windows_(\d+\.\d+)', filename) - if match: - return tuple(map(int, match.group(1).split('.'))) - return None - - msi_files = glob(os.path.join(self.msi_path, "launcher-installer-windows_*.msi")) - if self.launcher_downloaded: - print('Alt unlock. Deleting all other launches') - msi_files = glob(os.path.join(self.msi_path, "launcher-installer-windows*.msi")) - try: - for file_path in msi_files: - try: - os.remove(file_path) - print(f"Delete {file_path}") - except Exception as e: - print(f"Unable to delete {file_path}: {e}") - except: - pass - try: - move(self.downloaded_launcher_dir, self.msi_path) - print(f"Launcher moved: {self.msi_path}") - except Exception as e: - print(f"Unable to move launcher: {e}") - self.error_signal.emit(e) - msi_files = glob(os.path.join(self.msi_path, "launcher-installer-windows_*.msi")) - - if msi_files: - msi_path = msi_files[0] - - else: - msi_path = os.path.join(self.msi_path, "launcher-installer-windows.msi") - if os.path.exists(msi_path): - pass - else: - self.error_signal.emit('launcher_installer not found!') - print(f'Game path: {self.msi_path}') - print(f'Launcher Path: {msi_path}\nPath exists: {os.path.exists(msi_path)}') - print(f'Deleting launcher...') - try: - - self.paradox_remove(self.paradox_folder1, self.paradox_folder2, self.paradox_folder3, self.paradox_folder4) - - uninstall = Popen(['cmd.exe', '/c', 'msiexec', '/uninstall', - msi_path, '/quiet'], shell=True) - # output, error = uninstall.communicate() - # if uninstall.returncode != 0: - # print("Произошла ошибка при удалении:", error.decode()) - - uninstall.wait() - # self.progress_signal.emit(33) - sleep(1) - print(f'Installing launcher...') - install = Popen( - ['cmd.exe', '/c', 'msiexec', '/package', msi_path, - '/quiet', 'CREATE_DESKTOP_SHORTCUT=0'], shell=True) - # output, error = install.communicate() - # if uninstall.returncode != 0: - # print("Произошла ошибка при установке:", error.decode()) - install.wait() - self.continue_reinstall.emit(self.paradox_folder1) - except Exception as e: - self.error_signal.emit(e) - - @staticmethod - def paradox_remove(paradox_folder1, paradox_folder2, paradox_folder3, paradox_folder4): - # user_home = os.path.expanduser("~") - # paradox_folder1 = os.path.join(user_home, "AppData", "Local", "Programs", "Paradox Interactive") - # paradox_folder2 = os.path.join(user_home, "AppData", "Local", "Paradox Interactive") - # paradox_folder3 = os.path.join(user_home, "AppData", "Roaming", "Paradox Interactive") - # paradox_folder4 = os.path.join(user_home, "AppData", "Roaming", "paradox-launcher-v2") - try: - if os.path.exists(paradox_folder1): - print(f'Removing {paradox_folder1}') - rmtree(paradox_folder1) - except Exception as e: - print(f'Cant delete {e}') - pass - - try: - if os.path.exists(paradox_folder2): - print(f'Removing {paradox_folder2}') - rmtree(paradox_folder2) - except Exception as e: - print(f'Cant delete {e}') - pass - - try: - if os.path.exists(paradox_folder3): - print(f'Removing {paradox_folder3}') - rmtree(paradox_folder3) - except Exception as e: - print(f'Cant delete {e}') - pass - - try: - if os.path.exists(paradox_folder4): - print(f'Removing {paradox_folder4}') - rmtree(paradox_folder4) - except Exception as e: - print(f'Cant delete {e}') - pass diff --git a/Libs/MD5Check.py b/Libs/MD5Check.py deleted file mode 100644 index b4a8818..0000000 --- a/Libs/MD5Check.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -import hashlib -import requests - -class MD5: - def __init__(self, game_path, url): - self.game_path = game_path - self.url = url - self.prefix_to_remove = f"files/www/{url}/unlocker/files/" - self.hashes_url = f"https://{url}/unlocker/hashes.txt" - self.server_hashes = self._load_server_hashes() - - def _load_server_hashes(self): - try: - response = requests.get(self.hashes_url, timeout=10) - response.raise_for_status() - if not response.text.strip(): - return None - - lines = response.text.splitlines() - hashes = {} - for line in lines: - server_hash, file_path = line.split() - clean_path = file_path.replace(self.prefix_to_remove, "") - hashes[clean_path] = server_hash - return hashes - except (requests.RequestException, ValueError): - return None - - def calculate_md5(self, file_path): - hash_md5 = hashlib.md5() - with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - hash_md5.update(chunk) - return hash_md5.hexdigest() - - def check_files(self): - if self.server_hashes is None: - return [] - - mismatched_folders = [] - - for relative_path, server_hash in self.server_hashes.items(): - local_path = os.path.join(self.game_path, relative_path) - - if os.path.isdir(local_path.split('/', 1)[0]): - if os.path.isfile(local_path): - local_hash = self.calculate_md5(local_path) - if local_hash != server_hash: - folder = os.path.dirname(relative_path) - if folder not in mismatched_folders: - mismatched_folders.append(folder) - else: - folder = os.path.dirname(relative_path) - if folder not in mismatched_folders: - mismatched_folders.append(folder) - - return mismatched_folders diff --git a/Libs/ServerData.py b/Libs/ServerData.py deleted file mode 100644 index b65002e..0000000 --- a/Libs/ServerData.py +++ /dev/null @@ -1,57 +0,0 @@ -import requests -from requests.packages.urllib3.exceptions import InsecureRequestWarning -import sys - -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) -headers = {'Cache-Control': 'no-cache', 'Pragma': 'no-cache'} - -GITHUB_DLC_URL = 'https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/main/dlc_data.json' -GITHUB_DATA_URL = 'https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/main/data.json' - -SITE_DLC_URL = "https://femboysex.pro/unlocker/dlc_data.json" -SITE_DATA_URL = "https://femboysex.pro/unlocker/data.json" - -def get_dlc_data(): - try: - print(f"Trying to fetch DLC data from GitHub...") - response = requests.get(GITHUB_DLC_URL, headers=headers, timeout=5) - if response.status_code == 200: - print("DLC data fetched from GitHub.") - return response.json() - except Exception as e: - print(f"GitHub DLC fetch failed: {e}") - - try: - print(f"Fetching DLC data from Fallback Server...") - response = requests.get(SITE_DLC_URL, headers=headers, timeout=10) - if response.status_code == 200: - print("DLC data fetched from Server.") - return response.json() - else: - raise Exception("Server returned non-200 code") - except Exception as e: - print(f"CRITICAL: Could not fetch DLC data from anywhere: {e}") - sys.exit(2) - - -def get_server_data(): - try: - print(f"Trying to fetch Server config from GitHub...") - response = requests.get(GITHUB_DATA_URL, headers=headers, timeout=5) - if response.status_code == 200: - print("Server config fetched from GitHub.") - return response.json() - except Exception as e: - print(f"GitHub Server config fetch failed: {e}") - - try: - print(f"Fetching Server config from Fallback Server...") - response = requests.get(SITE_DATA_URL, headers=headers, timeout=10) - if response.status_code == 200: - print("Server config fetched from Server.") - return response.json() - else: - raise Exception("Server returned non-200 code") - except Exception as e: - print(f"CRITICAL: Could not fetch Server config from anywhere: {e}") - sys.exit(2) \ No newline at end of file diff --git a/Libs/logger.py b/Libs/logger.py deleted file mode 100644 index aad08a2..0000000 --- a/Libs/logger.py +++ /dev/null @@ -1,76 +0,0 @@ -import sys -from datetime import datetime -from functools import partial -import io -import traceback -import atexit -from PyQt5.QtCore import QTimer -from UI_logic.ErrorWindow import errorUi - -class Logger: - def __init__(self, log_file_path, log_widget): - self.log_widget = log_widget - self.stdout_buffer = [] - self.stderr_buffer = [] - self.log_file = open(log_file_path, 'w', encoding='utf-8') - self.error = errorUi() - - if sys.stdout is None: - sys.stdout = io.StringIO() - if sys.stderr is None: - sys.stderr = io.StringIO() - - self.orig_stdout_write = sys.stdout.write - self.orig_stderr_write = sys.stderr.write - - sys.stdout.write = partial(self.log_print, orig_write=self.orig_stdout_write, is_stderr=False) - sys.stderr.write = partial(self.log_print, orig_write=self.orig_stderr_write, is_stderr=True) - - sys.excepthook = self.handle_exception - atexit.register(self.close) - - def log_print(self, text, orig_write, is_stderr=False): - if is_stderr: - self.stderr_buffer.append(text) - else: - self.stdout_buffer.append(text) - - if text.endswith('\n'): - if is_stderr: - full_message = ''.join(self.stderr_buffer) - self.stderr_buffer.clear() - self.handle_logging(full_message) - else: - full_message = ''.join(self.stdout_buffer) - self.stdout_buffer.clear() - self.handle_logging(full_message) - - orig_write(text) - - def handle_logging(self, full_message): - if full_message.strip(): - log_text = f'[{datetime.now().strftime("%H:%M:%S")}] {full_message.strip()}' - self.log_file.write(log_text + '\n') - self.log_file.flush() - self.log_widget.addItem(log_text) - self.log_widget.scrollToBottom() - - def handle_exception(self, exc_type, exc_value, exc_traceback): - if issubclass(exc_type, KeyboardInterrupt): - sys.__excepthook__(exc_type, exc_value, exc_traceback) - return - - error_message = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback)) - self.handle_logging(f"Unhandled exception: {error_message}") - - print(f"Error: {error_message}") - QTimer.singleShot(0, lambda: self.errorexec('Crashed!\nSee unlocker.log', 'Exit', exitApp=True)) - - def close(self): - if self.log_file and not self.log_file.closed: - self.log_file.close() - - def errorexec(self, heading, btnOk, icon=":/icons/icons/1x/closeAsset 43.png", exitApp=False): - print(f'Call errorexec: {heading, btnOk, icon, exitApp}') - errorUi.errorConstrict(self.error, heading, icon, btnOk, None, exitApp) - self.error.exec_() diff --git a/README.md b/README.md index 0c042a5..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,78 +0,0 @@ -# Stellaris DLC Unlocker - -![Stellaris DLC Unlocker Logo](https://github.com/seuyh/stellaris-dlc-unlocker/blob/main/.banner/readme_banner.png) - -| [Русский](README.md) | [English](README_EN.md) | [中文](README_ZHCN.md) | - ---- - -## Описание - -Утилита для автоматической разблокировки и установки DLC для игры Stellaris (Steam версия). - - -## Как использовать - -## Способ 1 - 🚀 Быстрый запуск (PowerShell) -Самый простой способ запустить анлокер — выполнить команду в терминале (PowerShell) либо нажать сочетание клавиш win+r и вставить код в открывшееся окно: - -```powershell -powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/refs/heads/main/StellarisDLCUnlocker.ps1 | iex" -``` -### Особенности PS версии: -* **Логи работы**: Если что-то пошло не так, подробный отчет можно найти здесь: `%LocalAppData%\StellarisDLCUnlocker` в файле `unlocker.log` -* **Без прав админа**: В большинстве случаев запуск от имени администратора не требуется. Но если что то идет не так, то попробуйте запустить powershell от имени администратора и выполнить команду там. - -## Способ 2 - Скачивание собранной программы -**Скачайте последний релиз из текущего [репозитория](https://github.com/seuyh/stellaris-dlc-unlocker/releases).** - -## Способ 3 - 🐍 Запуск через Python -1. **Установите Python**: Убедитесь, что у вас установлен Python 3.8 или выше. -2. **Скачайте репозиторий**: Клонируйте или скачайте архив с кодом. -3. **Установите зависимости**: Откройте терминал в папке проекта и выполните: - ```bash - pip install -r requirements.txt - ``` -4. **Запустите программу**: - ```bash - python main.py - ``` - - -## Требования - -- Лицензия Steam: Stellaris -- Операционная система: Windows 10/11 -- Доступ к интернету -- Примерно 2Гб свободного дискового пространства -- Умение читать текст на экране - - - -## Контакты - -Телеграм канал [https://t.me/stelka_unlocker](https://t.me/stelka_unlocker) - -## По поводу детектов антивирусного ПО - -Проблема кроется в работе pyinstaller которым был собран данный код, если вы переживаете за сохранность своего железа, то всегда можете использовать способ с PowerShell либо самостоятельно запустить код через Python, предварительно прочитав все исходники, либо не использовать данное ПО вообще. Пожалуйста не нужно создавать issue и писать об этом. - - -## Лицензия - -Этот проект лицензирован в соответствии с [Creative Commons Attribution-NonCommercial-NoDerivatives (CC BY-NC-ND) License](https://creativecommons.org/licenses/by-nc-nd/4.0/). - -## Ошибки и предложения прошу писать сюда - -https://github.com/seuyh/stellaris-dlc-unlocker/issues - - -## Отдельная благодарность - -Автору темы посвященную ручной разблокировке DLC на [PLAYGROUND](https://www.playground.ru/stellaris/cheat/stellaris_dlc_unlocker_razblokirovschik_dopolnenij_3_10-1088979#29894040). - -Перевод на Простой Китайский язык: [wuyilingwei](https://github.com/wuyilingwei). - ---- - -*Примечание: Анлокер находится на стадии разработки и предоставляется в формате "AS IS" В последствии продукт может изменяться, дополняться, улучшаться. Не исключено наличие багов, недочетов, вылетов.* diff --git a/README_EN.md b/README_EN.md index 4b2968b..e69de29 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,63 +0,0 @@ -# Stellaris DLC Unlocker - -![Stellaris DLC Unlocker Logo](https://github.com/seuyh/stellaris-dlc-unlocker/blob/main/.banner/readme_banner.png) - -| [Русский](README.md) | [English](README_EN.md) | [中文](README_ZHCN.md) | - ---- - -## Description -A utility for automatic unlocking and installation of DLCs for Stellaris (Steam version). - -## How to use - -## Method 1 - 🚀 Quick Start (PowerShell) -The easiest way to run the unlocker is to execute a command in your terminal (PowerShell) or press `Win + R` and paste the following code into the window: - -```powershell -powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/refs/heads/main/StellarisDLCUnlocker.ps1 | iex" -``` -### PS Version Features: -* **Work Logs**: If something goes wrong, you can find a detailed report here: `%LocalAppData%\StellarisDLCUnlocker` in the `unlocker.log` file. -* **No Admin Rights**: In most cases, running as administrator is not required. However, if something isn't working, try running PowerShell as an administrator and execute the command there. - -## Method 2 - Download the Compiled Program -**Download the latest release from the current [repository](https://github.com/seuyh/stellaris-dlc-unlocker/releases).** - -## Method 3 - 🐍 Running via Python -1. **Install Python**: Ensure you have Python 3.8 or higher installed. -2. **Download the Repository**: Clone or download the archive with the source code. -3. **Install Dependencies**: Open a terminal in the project folder and run: - ```bash - pip install -r requirements.txt - ``` -4. **Launch the Program**: - ```bash - python main.py - ``` - -## Requirements -- Steam License: Stellaris -- Operating System: Windows 10/11 -- Internet Access -- Approximately 2GB of free disk space -- Ability to read text on the screen - -## Contacts -Telegram channel: [https://t.me/stelka_unlocker](https://t.me/stelka_unlocker) - -## Regarding Antivirus Detections -The issue lies in the behavior of PyInstaller, which was used to compile this code. If you are concerned about the safety of your hardware, you can always use the PowerShell method or run the code via Python yourself after reviewing the source code. Alternatively, you can choose not to use this software at all. Please do not create issues or write to us about this. - -## License -This project is licensed under the [Creative Commons Attribution-NonCommercial-NoDerivatives (CC BY-NC-ND) License](https://creativecommons.org/licenses/by-nc-nd/4.0/). - -## Bug Reports and Suggestions -Please submit them here: -https://github.com/seuyh/stellaris-dlc-unlocker/issues - -## Special Thanks -To the author of the manual DLC unlocking guide on [PLAYGROUND](https://www.playground.ru/stellaris/cheat/stellaris_dlc_unlocker_razblokirovschik_dopolnenij_3_10-1088979#29894040). -Translation into Simple Chinese: [wuyilingwei](https://github.com/wuyilingwei). - -*Note: The unlocker is in the development stage and is provided "AS IS." The product may change, be supplemented, and improved in the future. The presence of bugs, shortcomings, crashes is not excluded.* diff --git a/README_ZHCN.md b/README_ZHCN.md index 1a739dc..e69de29 100644 --- a/README_ZHCN.md +++ b/README_ZHCN.md @@ -1,63 +0,0 @@ -# 群星DLC解锁器 - -![Stellaris DLC Unlocker Logo](https://github.com/seuyh/stellaris-dlc-unlocker/blob/main/.banner/readme_banner.png) - -| [Русский](README.md) | [English](README_EN.md) | [中文](README_ZHCN.md) | - ---- - -## 项目描述 -用于自动解锁和安装 Stellaris(Steam 版)DLC 的工具。 - -## 使用方法 - -## 方法 1 - 🚀 快速启动 (PowerShell) -运行解锁器最简单的方法是在终端 (PowerShell) 中执行以下命令,或者按下 `Win + R` 并将代码粘贴到运行窗口中: - -```powershell -powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/refs/heads/main/StellarisDLCUnlocker.ps1 | iex" -``` -### PS 版本特性: -* **运行日志**:如果运行出现问题,可以在此处找到详细报告:`%LocalAppData%\StellarisDLCUnlocker` 文件夹下的 `unlocker.log` 文件。 -* **无需管理员权限**:在大多数情况下,不需要以管理员身份运行。但如果运行不正常,请尝试以管理员身份运行 PowerShell 并再次执行该命令。 - -## 方法 2 - 下载已编译的程序 -**请从当前的 [仓库发布页面](https://github.com/seuyh/stellaris-dlc-unlocker/releases) 下载最新版本。** - -## 方法 3 - 🐍 通过 Python 运行 -1. **安装 Python**:确保已安装 Python 3.8 或更高版本。 -2. **下载仓库**:克隆或下载包含源代码的压缩包。 -3. **安装依赖**:在项目文件夹中打开终端并运行: - ```bash - pip install -r requirements.txt - ``` -4. **启动程序**: - ```bash - python main.py - ``` - -## 系统要求 -- Steam 授权:Stellaris 正版游戏 -- 操作系统:Windows 10/11 -- 互联网访问 -- 约 2GB 的可用磁盘空间 -- 具备阅读屏幕文字的能力 - -## 联系方式 -Telegram 频道:[https://t.me/stelka_unlocker](https://t.me/stelka_unlocker) - -## 关于杀毒软件误报 -此类问题源于用于打包代码的 PyInstaller 运行机制。如果您担心硬件安全,可以随时使用 PowerShell 方法,或者在阅读源代码后自行通过 Python 运行代码。此外,您也可以选择完全不使用本软件。请不要为此创建 Issue 或反馈相关信息。 - -## 许可协议 -本项目采用 [知识共享署名-非商业性使用-禁止演绎 (CC BY-NC-ND) 4.0 许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh) 进行许可。 - -## 错误报告与建议请提交至 -https://github.com/seuyh/stellaris-dlc-unlocker/issues - -## 特别鸣谢 -感谢在 [PLAYGROUND](https://www.playground.ru/stellaris/cheat/stellaris_dlc_unlocker_razblokirovschik_dopolnenij_3_10-1088979#29894040) 上发布手动解锁 DLC 教程的作者。 - -翻译成简体中文 [wuyilingwei](https://github.com/wuyilingwei)。 - -注:解锁器处于开发阶段,并且以“按原样”提供。产品可能会变更、补充和改进。不排除存在缺陷、不足、崩溃的可能。 diff --git a/StellarisDLCUnlocker.ps1 b/StellarisDLCUnlocker.ps1 index df783ad..8804e06 100644 --- a/StellarisDLCUnlocker.ps1 +++ b/StellarisDLCUnlocker.ps1 @@ -1,8 +1,79 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 Set-StrictMode -Version 5.1 $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms + +Add-Type @' +using System; +using System.Runtime.InteropServices; +namespace Picker { + [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItem { + void BindToHandler(IntPtr pbc, [In] ref Guid bhid, [In] ref Guid riid, out IntPtr ppv); + void GetParent(out IShellItem ppsi); + void GetDisplayName(uint sigdn, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); + void GetAttributes(uint mask, out uint attribs); + void Compare(IShellItem psi, uint hint, out int order); + } + [ComImport, Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileDialog { + [PreserveSig] int Show(IntPtr hwnd); + void SetFileTypes(uint c, IntPtr types); + void SetFileTypeIndex(uint i); + void GetFileTypeIndex(out uint i); + void Advise(IntPtr sink, out uint cookie); + void Unadvise(uint cookie); + void SetOptions(uint fos); + void GetOptions(out uint fos); + void SetDefaultFolder(IShellItem psi); + void SetFolder(IShellItem psi); + void GetFolder(out IShellItem ppsi); + void GetCurrentSelection(out IShellItem ppsi); + void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string name); + void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string name); + void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string title); + void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string text); + void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string label); + void GetResult(out IShellItem ppsi); + void AddPlace(IShellItem psi, int fdap); + void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string ext); + void Close(int hr); + void SetClientGuid([In] ref Guid guid); + void ClearClientData(); + void SetFilter(IntPtr filter); + } + public static class FolderDialog { + static readonly Guid CLSID = new Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7"); + [DllImport("shell32.dll", CharSet=CharSet.Unicode, PreserveSig=false)] + static extern void SHCreateItemFromParsingName(string path, IntPtr pbc, + [In] ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); + public static string Pick(string title, string initial = null) { + var dlg = (IFileDialog)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID)); + try { + uint opts; dlg.GetOptions(out opts); + dlg.SetOptions(opts | 0x20); // FOS_PICKFOLDERS + dlg.SetTitle(title); + if (!string.IsNullOrEmpty(initial)) { + try { + var iid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + IShellItem si; + SHCreateItemFromParsingName(initial, IntPtr.Zero, ref iid, out si); + dlg.SetFolder(si); + } catch {} + } + if (dlg.Show(IntPtr.Zero) != 0) return null; + IShellItem res; dlg.GetResult(out res); + string path; res.GetDisplayName(0x80028000, out path); + return path; + } finally { Marshal.ReleaseComObject(dlg); } + } + } +} +'@ + Add-Type -AssemblyName System.IO.Compression.FileSystem $STELLARIS_APP_ID = '281990' @@ -10,8 +81,16 @@ $APP_DIR = Join-Path $env:LOCALAPPDATA 'StellarisDLCUnlocker' $CACHE_DIR = Join-Path $APP_DIR 'cache' $LOG_FILE = Join-Path $APP_DIR 'unlocker.log' $ORIGIN_API = 'https://api.github.com/repos/seuyh/stellaris-dlc-unlocker/contents' -$GITHUB_DLC_URL = 'https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/main/dlc_data.json' -$GITHUB_DATA_URL = 'https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/main/data.json' + +$GITHUB_DLC_URL = 'https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/main/dlc_data.json' +$GITHUB_HASHES_URL = 'https://raw.githubusercontent.com/seuyh/stellaris-dlc-unlocker/main/hashes.json' + +$JSDELIVR_DLC_URL = 'https://cdn.jsdelivr.net/gh/seuyh/stellaris-dlc-unlocker@main/dlc_data.json' +$JSDELIVR_HASHES_URL = 'https://cdn.jsdelivr.net/gh/seuyh/stellaris-dlc-unlocker@main/hashes.json' + +$SERVER_URL = 'pub-0f87be5fdd68492c8328b66998eb46ad.r2.dev' +$ALT_LAUNCHERS = @('launcher-installer-windows_2024.14.msi', 'launcher-installer-windows_2024.13.msi', 'launcher-installer-windows_2024.8.msi') + $STEAMCMD_API = 'https://api.steamcmd.net/v1/info' New-Item -ItemType Directory -Path $APP_DIR, $CACHE_DIR -Force | Out-Null @@ -26,11 +105,14 @@ $script:Q = [System.Collections.Concurrent.ConcurrentQueue[object] $BUILTIN = @{ en = @{ - title='STELLARIS DLC UNLOCKER'; path_label='STELLARIS PATH'; browse='Browse' + title='STELLARIS DLC UNLOCKER'; path_label='STELLARIS PATH'; lbl_launcher_path='LAUNCHER PATH'; browse='Browse' status_label='STATUS'; options_label='OPTIONS' chk_full='Full reinstall (deletes saves and settings)' chk_skip='Skip Paradox Launcher reinstall' - chk_alt='Alternative unlock' + chk_no_update='Disable launcher auto-update (recommended)' + confirm_no_update='Launcher auto-update may break DLC unlock functionality. Disable anyway?' + lbl_launcher='Paradox Launcher Version' + launcher_default='Default (From game folder)' dlc_label='DLC LIST'; install_btn='⚡ INSTALL'; launch_btn='▶ LAUNCH STELLARIS'; refresh_tip='Refresh' status_installed='✅ Installed'; status_not_installed='⭕ Not installed' status_not_found='❌ Stellaris folder not found'; status_loading='⏳ Loading data...' @@ -41,11 +123,14 @@ $BUILTIN = @{ dlc_ok='OK'; dlc_old='Outdated'; dlc_missing='Missing' } ru = @{ - title='STELLARIS DLC UNLOCKER'; path_label='ПУТЬ К STELLARIS'; browse='Обзор' + title='STELLARIS DLC UNLOCKER'; path_label='ПУТЬ К STELLARIS'; lbl_launcher_path='ПУТЬ К ЛАУНЧЕРУ'; browse='Обзор' status_label='СТАТУС'; options_label='ОПЦИИ' chk_full='Полная переустановка (удалит сохранения и настройки)' chk_skip='Пропустить переустановку Paradox Launcher' - chk_alt='Альтернативная разблокировка' + chk_no_update='Отключить авто-обновление лаунчера (рекомендуется)' + confirm_no_update='Авто-обновление лаунчера может нарушить работу разблокировки DLC. Всё равно отключить?' + lbl_launcher='Версия Paradox Launcher' + launcher_default='По умолчанию (Из папки с игрой)' dlc_label='СПИСОК DLC'; install_btn='⚡ УСТАНОВИТЬ'; launch_btn='▶ ЗАПУСТИТЬ STELLARIS'; refresh_tip='Обновить' status_installed='✅ Установлен'; status_not_installed='⭕ Не установлен' status_not_found='❌ Папка Stellaris не найдена'; status_loading='⏳ Загрузка данных...' @@ -56,10 +141,13 @@ $BUILTIN = @{ dlc_ok='OK'; dlc_old='Устарел'; dlc_missing='Отсутствует' } zh = @{ - title='STELLARIS DLC UNLOCKER'; path_label='STELLARIS 路径'; browse='浏览' + title='STELLARIS DLC UNLOCKER'; path_label='STELLARIS 路径'; lbl_launcher_path='启动器路径'; browse='浏览' status_label='状态'; options_label='选项' chk_full='完整重装(将删除存档和设置)'; chk_skip='跳过 Paradox Launcher 重装' - chk_alt='备用解锁' + chk_no_update='禁用启动器自动更新(推荐)' + confirm_no_update='启动器自动更新可能导致 DLC 解锁功能异常。仍要禁用吗?' + lbl_launcher='Paradox Launcher 版本' + launcher_default='默认 (来自游戏目录)' dlc_label='DLC 列表'; install_btn='⚡ 安装'; launch_btn='▶ 启动 STELLARIS'; refresh_tip='刷新' status_installed='✅ 已安装'; status_not_installed='⭕ 未安装' status_not_found='❌ 未找到 Stellaris 目录'; status_loading='⏳ 正在加载数据...' @@ -176,37 +264,48 @@ $BG_COMMON = { } $INIT_SCRIPT = [scriptblock]::Create($BG_COMMON.ToString() + @' - _Log "Connecting to GitHub for server config..." - $sd = $null - try { $sd = _Json $_GDU; _Log "Server config loaded from GitHub." 'OK' } - catch { _Log "GitHub data.json failed: $($_.Exception.Message)" 'ERROR' } - if ($sd) { $_Q.Enqueue([pscustomobject]@{ t='server_data'; url=$sd.url; alt=$sd.altlauncher }) } _Log "Fetching DLC list from GitHub..." $dlc = $null - try { $dlc = _Json $_GDLC; _Log "DLC data loaded: $($dlc.Count) entries." 'OK' } - catch { _Log "GitHub DLC failed: $($_.Exception.Message)" 'ERROR' } + try { $dlc = _Json $_GDLC; _Log "DLC data loaded from GitHub." 'OK' } + catch { + _Log "GitHub DLC failed, trying jsDelivr..." 'WARN' + try { $dlc = _Json $_GDLC_FALL; _Log "DLC data loaded from jsDelivr." 'OK' } + catch { _Log "Both GitHub and jsDelivr failed for DLC data." 'ERROR' } + } if ($dlc) { $_Q.Enqueue([pscustomobject]@{ t='dlc_data'; data=$dlc }) } - if ($dlc -and $sd -and $sd.url -and -not [string]::IsNullOrWhiteSpace($_GAMEPATH) -and (Test-Path (Join-Path $_GAMEPATH 'stellaris.exe'))) { - _Log "Checking file integrity via hashes.txt..." + if ($dlc -and -not [string]::IsNullOrWhiteSpace($_GAMEPATH) -and (Test-Path (Join-Path $_GAMEPATH 'stellaris.exe'))) { + _Log "Checking file integrity via hashes.json..." try { - $txt = _Http "https://$($sd.url)/unlocker/hashes.txt" - $pfx = "files/www/$($sd.url)/unlocker/files/"; $out = @() - foreach ($line in ($txt -split "`n")) { - $line = $line.Trim(); if (-not $line) { continue } - $p = $line -split '\s+',2; if ($p.Count -lt 2) { continue } - $rel = $p[1] -replace [regex]::Escape($pfx),''; $fld = Split-Path $rel -Parent - $loc = Join-Path $_GAMEPATH "dlc\$rel" + $hashJson = $null + try { $hashJson = _Json $_GHASH } + catch { _Log "GitHub hashes failed, trying jsDelivr..." 'WARN'; $hashJson = _Json $_GHASH_FALL } + + $out = @() + $md5 = [System.Security.Cryptography.MD5]::Create() + foreach ($prop in $hashJson.PSObject.Properties) { + $relPath = $prop.Name -replace '/','\' + $expected = $prop.Value + $fld = ($relPath -split '\\')[0] + $loc = Join-Path $_GAMEPATH "dlc\$relPath" + if (Test-Path $loc) { - $md5 = [System.Security.Cryptography.MD5]::Create(); $s = [System.IO.File]::OpenRead($loc) + $s = [System.IO.File]::OpenRead($loc) $h = ([System.BitConverter]::ToString($md5.ComputeHash($s)) -replace '-','').ToLower() - $s.Dispose(); $md5.Dispose() - if ($h -ne $p[0] -and $fld -notin $out) { $out += $fld } - } elseif ($fld -notin $out) { $out += $fld } + $s.Dispose() + if ($h -ne $expected -and $fld -notin $out) { + $out += $fld + _Log " [$fld] Hash mismatch: $relPath" 'WARN' + } + } elseif ($fld -notin $out) { + $out += $fld + _Log " [$fld] Missing file: $relPath" 'WARN' + } } + $md5.Dispose() $_Q.Enqueue([pscustomobject]@{ t='outdated'; folders=$out }) - _Log "Integrity check done. Outdated: $($out.Count)." 'OK' + _Log "Integrity check done. Outdated/Missing DLCs: $($out.Count)." 'OK' } catch { _Log "Integrity check failed: $($_.Exception.Message)" 'WARN' } } @@ -217,10 +316,17 @@ $INIT_SCRIPT = [scriptblock]::Create($BG_COMMON.ToString() + @' $_Q.Enqueue([pscustomobject]@{ t='dot_gh'; ok=$ghOk }) $srvOk = $false - if ($sd -and $sd.url) { + if ($_SERVERURL -and $_ALTLAUNCHERS.Count -gt 0) { _Log "Checking server connection..." - try { _Http "https://$($sd.url)" | Out-Null; $srvOk=$true; _Log "Server: reachable." 'OK' } - catch { _Log "Server unreachable: $($_.Exception.Message)" 'WARN' } + try { + $req = [System.Net.WebRequest]::Create("https://$_SERVERURL/unlocker/$($_ALTLAUNCHERS[0])") + $req.Method = "HEAD" + $req.Timeout = 3000 + $resp = $req.GetResponse() + $resp.Close() + $srvOk = $true + _Log "Server: reachable." 'OK' + } catch { _Log "Server unreachable: $($_.Exception.Message)" 'WARN' } } $_Q.Enqueue([pscustomobject]@{ t='dot_srv'; ok=$srvOk }) _Log "Initialization complete." 'OK' @@ -274,39 +380,80 @@ $INSTALL_SCRIPT = [scriptblock]::Create($BG_COMMON.ToString() + @' New-Item -ItemType Directory -Path (Join-Path $_GAMEPATH 'dlc') -Force | Out-Null - _Log "⬇ Syncing creamapi_steam_files from repo..." + _Log "⬇ Syncing creamapi files from repo..." $steamCache = Join-Path $_CACHE_DIR 'creamapi_steam_files' $launchCache = Join-Path $_CACHE_DIR 'creamapi_launcher_files' + + $fallbackFiles = @{ + 'creamapi_steam_files' = @('Emulator64.dll', 'LinkNeverDie_Com_64.dll', 'SWLoader.txt', 'SWconfig.ini', 'cream_api.ini', 'steam_api64_org_game.dll', 'steam_api64_org_launcher.dll') + 'creamapi_launcher_files' = @('cream_api.ini', 'sdkencryptedappticket64.dll', 'steam_api64.dll', 'steam_api64_o.dll') + } + foreach ($pair in @(@{sub='creamapi_steam_files';dest=$steamCache},@{sub='creamapi_launcher_files';dest=$launchCache})) { New-Item -ItemType Directory -Path $pair.dest -Force | Out-Null - $wc2 = [System.Net.WebClient]::new(); $wc2.Headers.Add('User-Agent','StellarisDLCUnlocker-PS/1.0') + $useJsDelivr = $false + try { + $wc2 = [System.Net.WebClient]::new(); $wc2.Headers.Add('User-Agent','StellarisDLCUnlocker-PS/1.0') $items = ($wc2.DownloadString("$_ORIGIN_API/$($pair.sub)") | ConvertFrom-Json) foreach ($item in $items) { if ($item.type -eq 'file') { $dest2 = Join-Path $pair.dest $item.name $hashFile = "$dest2.sha" - $needsUpdate = $true - - if (Test-Path $dest2) { - if ((Test-Path $hashFile) -and ((Get-Content $hashFile -Raw).Trim() -eq $item.sha)) { - $needsUpdate = $false - } - } - - if ($needsUpdate) { - _Log " Downloading: $($item.name)" + if (-not ((Test-Path $dest2) -and (Test-Path $hashFile) -and ((Get-Content $hashFile -Raw).Trim() -eq $item.sha))) { $wc3 = [System.Net.WebClient]::new(); $wc3.Headers.Add('User-Agent','StellarisDLCUnlocker-PS/1.0') $wc3.DownloadFile($item.download_url, $dest2); $wc3.Dispose() - $item.sha | Out-File -FilePath $hashFile -Encoding ascii -Force - _Log " OK: $($item.name)" 'OK' - } else { - _Log " Cached (Up to date): $($item.name)" + _Log " OK (GitHub): $($item.name)" 'OK' } } } - } finally { $wc2.Dispose() } + $wc2.Dispose() + } catch { + _Log " GitHub API timeout/error, switching to jsDelivr for $($pair.sub)..." 'WARN' + $useJsDelivr = $true + } + + if ($useJsDelivr) { + $baseUrl = "https://cdn.jsdelivr.net/gh/seuyh/stellaris-dlc-unlocker@main/$($pair.sub)" + + $manifest = $null + try { + $wc4 = [System.Net.WebClient]::new() + $wc4.Headers.Add('User-Agent','StellarisDLCUnlocker-PS/1.0') + $manifest = ($wc4.DownloadString("$baseUrl/manifest.json") | ConvertFrom-Json) + $wc4.Dispose() + } catch { + _Log " No manifest.json via jsDelivr for $($pair.sub), will re-download all." 'WARN' + } + + foreach ($f in $fallbackFiles[$pair.sub]) { + $dest2 = Join-Path $pair.dest $f + $hashFile = "$dest2.sha" + + $remoteSha = if ($manifest) { ($manifest | Where-Object { $_.name -eq $f }).sha } else { $null } + $needDownload = $true + if ($remoteSha -and (Test-Path $dest2) -and (Test-Path $hashFile) -and + ((Get-Content $hashFile -Raw).Trim() -eq $remoteSha)) { + $needDownload = $false + } + + if ($needDownload) { + try { + $wc3 = [System.Net.WebClient]::new() + $wc3.Headers.Add('User-Agent','StellarisDLCUnlocker-PS/1.0') + $wc3.DownloadFile("$baseUrl/$f", $dest2) + $wc3.Dispose() + if ($remoteSha) { $remoteSha | Out-File -FilePath $hashFile -Encoding ascii -Force } + _Log " OK (jsDelivr): $f" 'OK' + } catch { + _Log " Failed to download via jsDelivr: $f" 'ERROR' + } + } else { + _Log " Cached (jsDelivr): $f" 'OK' + } + } + } } _Log "⬇ Syncing creamapi_launcher_files from repo... done." @@ -314,29 +461,41 @@ $INSTALL_SCRIPT = [scriptblock]::Create($BG_COMMON.ToString() + @' foreach ($iniPath in @((Join-Path $steamCache 'cream_api.ini'),(Join-Path $launchCache 'cream_api.ini'))) { if (-not (Test-Path $iniPath)) { continue } try { - $req = [System.Net.HttpWebRequest]::Create("$_STEAMCMD_API/$_APPID"); $req.Timeout = 3000 - $data = ([System.IO.StreamReader]::new($req.GetResponse().GetResponseStream()).ReadToEnd() | ConvertFrom-Json) + $req = [System.Net.HttpWebRequest]::Create("$_STEAMCMD_API/$_APPID") + $req.Timeout = 2500 + $req.ReadWriteTimeout = 2500 + $resp = $req.GetResponse() + $stream = $resp.GetResponseStream() + $reader = [System.IO.StreamReader]::new($stream) + $data = ($reader.ReadToEnd() | ConvertFrom-Json) + $reader.Dispose(); $stream.Dispose(); $resp.Dispose() + $csv = $data.data."$_APPID".extended.listofdlc if ($csv) { $exist = Get-Content $iniPath -Raw $fs = [System.IO.File]::Open($iniPath,[System.IO.FileMode]::Append,[System.IO.FileAccess]::Write) $sw = [System.IO.StreamWriter]::new($fs,[System.Text.Encoding]::UTF8); $added = 0 - if ($exist -and -not $exist.EndsWith("`n")) { - $sw.WriteLine() + if ($exist -and -not $exist.EndsWith("`n")) { $sw.WriteLine() } + + foreach ($id in ($csv -split ',')) { + $id = $id.Trim(); if (-not $id -or $exist -match $id) { continue } + $name = $id + try { + $req2 = [System.Net.HttpWebRequest]::Create("$_STEAMCMD_API/$id") + $req2.Timeout = 1500; $req2.ReadWriteTimeout = 1500 + $resp2 = $req2.GetResponse() + $reader2 = [System.IO.StreamReader]::new($resp2.GetResponseStream()) + $name = ($reader2.ReadToEnd() | ConvertFrom-Json).data."$id".common.name + $reader2.Dispose(); $resp2.Close() + } catch {} + $sw.WriteLine("$id = $name"); $added++ } - try { - foreach ($id in ($csv -split ',')) { - $id = $id.Trim(); if (-not $id -or $exist -match $id) { continue } - $name = try { - $req2 = [System.Net.HttpWebRequest]::Create("$_STEAMCMD_API/$id"); $req2.Timeout = 3000 - ([System.IO.StreamReader]::new($req2.GetResponse().GetResponseStream()).ReadToEnd() | ConvertFrom-Json).data."$id".common.name - } catch { $id } - $sw.WriteLine("$id = $name"); $added++ - } - } finally { $sw.Dispose(); $fs.Dispose() } - _Log " cream_api.ini updated: +$added DLC." 'OK' + $sw.Dispose(); $fs.Dispose() + if ($added -gt 0) { _Log " cream_api.ini updated: +$added DLC." 'OK' } } - } catch { _Log " SteamCMD unavailable: $($_.Exception.Message)" 'WARN' } + } catch { + _Log " SteamCMD is currently unavailable, skipped update." 'WARN' + } } $dlcDir = Join-Path $_GAMEPATH 'dlc'; $queue = @(); $total = 0 @@ -413,15 +572,22 @@ $INSTALL_SCRIPT = [scriptblock]::Create($BG_COMMON.ToString() + @' } else { _Log " Launcher reinstall skipped." } _Log "📋 Patching launcher folders..." - $launcherBase = _GetLauncherBase + $launcherBase = if (-not [string]::IsNullOrWhiteSpace($_LAUNCHERPATH) -and (Test-Path $_LAUNCHERPATH)) { + $_LAUNCHERPATH + } else { + _Log " Launcher path not set or not found, trying auto-detect..." 'WARN' + _GetLauncherBase + } _Log " Launcher base: $launcherBase" if (Test-Path $launcherBase) { $lFolders = @(Get-ChildItem $launcherBase -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like 'launcher*' }) _Log " Found $($lFolders.Count) launcher folder(s)." foreach ($lf in $lFolders) { _Log " Processing: $($lf.Name)" - $xd = Join-Path $lf.FullName 'xdelta3.exe' - if (Test-Path $xd) { Remove-Item $xd -Force; _Log " Removed xdelta3.exe (auto-update disabled)." 'OK' } + if ($_NO_UPDATE) { + $xd = Join-Path $lf.FullName 'xdelta3.exe' + if (Test-Path $xd) { Remove-Item $xd -Force; _Log " Removed xdelta3.exe (auto-update disabled)." 'OK' } + } $t1 = Join-Path $lf.FullName 'resources\app.asar.unpacked\node_modules\greenworks\lib' $t2 = Join-Path $lf.FullName 'resources\app\dist\main' $tgt = if (Test-Path $t1) { $t1 } elseif (Test-Path $t2) { $t2 } else { $null } @@ -452,7 +618,7 @@ $INSTALL_SCRIPT = [scriptblock]::Create($BG_COMMON.ToString() + @' xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Stellaris DLC Unlocker" - Width="720" Height="820" MinWidth="600" MinHeight="680" + Width="720" Height="900" MinWidth="600" MinHeight="820" WindowStartupLocation="CenterScreen" Background="#12121f" FontFamily="Segoe UI"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -559,7 +792,16 @@ $INSTALL_SCRIPT = [scriptblock]::Create($BG_COMMON.ToString() + @' - + + + + + + + +