├── LICENSE ├── README.md ├── Steamless_CLI ├── Plugins │ ├── ExamplePlugin.dll │ ├── SharpDisasm.dll │ ├── Steamless.API.dll │ ├── Steamless.Unpacker.Variant10.x86.dll │ ├── Steamless.Unpacker.Variant20.x86.dll │ ├── Steamless.Unpacker.Variant21.x86.dll │ ├── Steamless.Unpacker.Variant30.x64.dll │ ├── Steamless.Unpacker.Variant30.x86.dll │ ├── Steamless.Unpacker.Variant31.x64.dll │ └── Steamless.Unpacker.Variant31.x86.dll ├── Steamless.CLI.exe ├── Steamless.CLI.exe.config └── infos.txt ├── icon_hashtag.ico ├── requirements.txt ├── sac_emu ├── dlc_creamapi │ ├── config_override.ini │ ├── files │ │ ├── cream_api.ini │ │ ├── steam_api.dll │ │ └── steam_api64.dll │ └── infos.txt ├── game_ali213 │ ├── files │ │ ├── SteamConfig.ini │ │ ├── steam_api.dll │ │ └── steam_api64.dll │ └── infos.txt └── game_goldberg │ ├── files │ ├── steam_api.dll │ ├── steam_api64.dll │ └── steam_settings │ │ ├── DLC.txt │ │ └── steam_appid.txt │ └── infos.txt ├── sac_lib ├── __init__.py └── get_file_version.py └── steam_auto_cracker_gui.py /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, BigBoiCJ 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SteamAutoCracker 2 | ![GitHub all releases](https://img.shields.io/github/downloads/BigBoiCJ/SteamAutoCracker/total?color=brightgreen&label=Total%20downloads) 3 | ![GitHub release (latest by date)](https://img.shields.io/github/downloads/BigBoiCJ/SteamAutoCracker/latest/total?color=green&label=Latest%20version%20downloads) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/BigBoiCJ/SteamAutoCracker?color=yellow&label=Stars) 5 | ![GitHub watchers](https://img.shields.io/github/watchers/BigBoiCJ/SteamAutoCracker?label=Watchers) 6 | 7 | An open-source script that automatically Cracks (removes DRM from) Steam games 8 | 9 | ## How to use (easy way) 10 | - Download the bundled/compiled version by clicking [here](https://github.com/BigBoiCJ/SteamAutoCracker/releases/latest) and downloading the file named `Steam.Auto.Cracker.GUI.vX.X.X.zip` 11 | - Extract the content of the archive (.zip) somewhere on your computer 12 | - Run `steam_auto_cracker_gui.exe` 13 | - Select the folder of your game 14 | - Enter the name of the game to try to crack it! (you can also enter the Steam AppID if you know it) 15 | - SAC will automatically attempt to find the AppID using the Name you provided. If it can't, please try entering the AppID yourself. 16 | - You can find the AppID in the URL of the game's Steam page (ex: store.steampowered.com/app/-> ***620980*** <-/Beat_Saber/) 17 | 18 | ## Features 19 | - Automatically cracks your bought or pirated Steam games. You only need to select the game's folder, and enter the Game Name or AppID. 20 | - Cracks **Steam API DRM** by applying and configuring **Steam Emulators** automatically 21 | - Cracks **Steam Stub DRM** by applying **Steamless** on executables automatically 22 | - No Steam account or Steam API key needed 23 | - Configurable to your liking 24 | - Option to only unlock DLCs for your bought Steam games instead of cracking them entirely 25 | - Option to choose your own Steam Emu thanks to a simple list, and simple config template system (default: ALI213) 26 | - List of Steam emus included by default: 27 | - ALI213 (Game) 28 | - Goldberg (Game) 29 | - CreamAPI (DLC) 30 | - Open source, transparent and privacy focused. No hidden analytics or weird things! 31 | - An opt-in autoupdater and version checker. Opt-in for privacy! 32 | 33 | ## Screenshots 34 | Screenshots from v2.0.0 35 | 36 |
37 | Images 38 | 39 | 40 | 41 |
42 | 43 | ## Requirements 44 | - An internet connection (SAC will do requests to `steampowered.com` to retrieve AppIDs and DLCs) 45 | - If you use the compiled .exe: 46 | - 64 bits Windows 47 | - If you use the python file (source): 48 | - The `requests` module. Install with `py -m pip install requests` or `python -m pip install requests` or `python3 -m pip install requests` 49 | - The `pywin32` module (which contains win32api). Install with `py -m pip install pywin32` or `python -m pip install pywin32` or `python3 -m pip install pywin32` 50 | - If you have any problem, please check https://pypi.org/project/pywin32/ 51 | - The `tkinter` module, but it should be included in Python by default. 52 | - As of v2.2.0 GUI, the `tkinterdnd2` module is required as well (v0.4.0+). Install it with `py -m pip install tkinterdnd2`. ([pypi link](https://pypi.org/project/tkinterdnd2/) - [github link](https://github.com/Eliav2/tkinterdnd2)) 53 | - I believe Python 3.7+ is needed. 54 | 55 | ## Notes about DLCs 56 | Some DLCs in some games requires you to download additional files.\ 57 | This tool is not able to download those files, you'll have to get a clean version of them. 58 | 59 | You can get clean Steam files for games (and sometimes DLCs) in the [Steam Content Sharing section from cs.rin.ru](https://cs.rin.ru/forum/viewforum.php?f=22) 60 | 61 | ## Windows Build informations 62 | Compiled using [pyinstaller](https://pypi.org/project/pyinstaller/) and venv\ 63 | Was previously compiled using [auto-py-to-exe](https://pypi.org/project/auto-py-to-exe/) (which is just a GUI for pyinstaller) 64 | 65 | Instructions on how to compile SAC, as well as useful scripts are available here: https://github.com/BigBoiCJ/SteamAutoCracker/tree/compile-env 66 | 67 | ## Privacy 68 | SAC will do requests to `steampowered.com` (Steam's official website) to retrieve AppIDs and DLCs.\ 69 | It is not bannable, and won't cause you problems. 70 | 71 | SAC will do requests to this GitHub repository to check for updates, download the autoupdater and new releases.\ 72 | This only happens if you decide to manually click on the "Check for updates" button, and decide to update using the autoupdater. SAC can also automatically check for updates if enabled in the settings (it is disabled by default) 73 | 74 | Nothing is logged by SAC.\ 75 | You can delete the SAC folder at any time and there won't be any leftovers. * 76 | 77 | __* Exception to leftovers:__ 78 | - There will be some leftovers if you use the compiled exe. This is due to how PyInstaller / auto-py-to-exe works. An embedded version of Python and the python script itself will be extracted to the temp-folder of your OS. The folder will be named `_MEIxxxxxx`, where xxxxxx is a random number. You can delete the folder at any time after using the program, as it might not correctly delete itself in all cases. Please check the [pyinstaller documentation](https://pyinstaller.org/en/stable/operating-mode.html#how-the-one-file-program-works) for more infos. 79 | 80 | ## Virus detection 81 | You could get a virus detection on some files. The biggest offender is `sac_emu/game_ali213/files/steam_api.dll`.\ 82 | A lot of cracking tools are detected as malware, either because their behavior is suspect (bypass game protections), or because they have been flagged manually (happens with a lot of tools).\ 83 | If you're suspicious about the legitimacy of the files, just delete the DLLs and use your owns instead.\ 84 | You can discuss with others about the tool in [cs.rin.ru](https://cs.rin.ru/forum/viewtopic.php?f=10&t=120610) or in the GitHub issues. 85 | 86 | ## Thanks 87 | - Thanks to [atom0s for their Steamless project](https://github.com/atom0s/Steamless) 88 | - Thanks to [oureveryday for their Steamless fork, supporting command-line](https://github.com/oureveryday/Steamless_CLI) (no longer used) 89 | - Thanks to the creators of Steam Emus, specifically those who are included: ALI213, Goldberg and deadmau5 (creator of CreamAPI) 90 | - Thanks to CS.RIN.RU and their members for being helpful and sharing quality uploads 91 | - Thanks to our contributors that propose code, report issues and give suggestions! The most notable ones will be quoted in releases' notes 92 | - Even if you're not credited, that doesn't mean you didn't help! I thank everyone :heart: 93 | -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/ExamplePlugin.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/ExamplePlugin.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/SharpDisasm.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/SharpDisasm.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.API.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.API.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.Unpacker.Variant10.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.Unpacker.Variant10.x86.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.Unpacker.Variant20.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.Unpacker.Variant20.x86.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.Unpacker.Variant21.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.Unpacker.Variant21.x86.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.Unpacker.Variant30.x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.Unpacker.Variant30.x64.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.Unpacker.Variant30.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.Unpacker.Variant30.x86.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.Unpacker.Variant31.x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.Unpacker.Variant31.x64.dll -------------------------------------------------------------------------------- /Steamless_CLI/Plugins/Steamless.Unpacker.Variant31.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Plugins/Steamless.Unpacker.Variant31.x86.dll -------------------------------------------------------------------------------- /Steamless_CLI/Steamless.CLI.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/Steamless_CLI/Steamless.CLI.exe -------------------------------------------------------------------------------- /Steamless_CLI/Steamless.CLI.exe.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Steamless_CLI/infos.txt: -------------------------------------------------------------------------------- 1 | The goal of this is to remove the SteamStub DRM from Steam games executable (.exe) files. 2 | 3 | The version included is: Steamless v3.1.0.5 (CLI) (source: https://github.com/atom0s/Steamless/releases/tag/v3.1.0.5) -------------------------------------------------------------------------------- /icon_hashtag.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/icon_hashtag.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pywin32 3 | tkinterdnd2 -------------------------------------------------------------------------------- /sac_emu/dlc_creamapi/config_override.ini: -------------------------------------------------------------------------------- 1 | [FileNames] 2 | steamapi = steam_api_o.dll 3 | steamapi64 = steam_api64_o.dll -------------------------------------------------------------------------------- /sac_emu/dlc_creamapi/files/cream_api.ini: -------------------------------------------------------------------------------- 1 | [steam] 2 | ; Application ID (http://store.steampowered.com/app/%appid%/) 3 | appid = SAC_AppID 4 | ; Current game language. 5 | ; Uncomment this option to turn it on. 6 | ; Default is "english". 7 | ;language = german 8 | ; Enable/disable automatic DLC unlock. Default option is set to "false". 9 | ; Keep in mind that this option WON'T work properly if the "[dlc]" section is NOT empty 10 | unlockall = false 11 | ; Original Valve's steam_api.dll. 12 | ; Default is "steam_api_o.dll". 13 | orgapi = steam_api_o.dll 14 | ; Original Valve's steam_api64.dll. 15 | ; Default is "steam_api64_o.dll". 16 | orgapi64 = steam_api64_o.dll 17 | ; Enable/disable extra protection bypasser. 18 | ; Default is "false". 19 | extraprotection = false 20 | ; The game will think that you're offline (supported by some games). 21 | ; Default is "false". 22 | forceoffline = false 23 | ; Some games are checking for the low violence presence. 24 | ; Default is "false". 25 | ;lowviolence = true 26 | ; Purchase timestamp for the DLC (http://www.onlineconversion.com/unix_time.htm). 27 | ; Default is "0" (1970/01/01). 28 | ;purchasetimestamp = 0 29 | 30 | [steam_misc] 31 | ; Disables the internal SteamUser interface handler. 32 | ; Does have an effect on the games that are using the license check for the DLC/application. 33 | ; Default is "false". 34 | disableuserinterface = false 35 | 36 | [dlc] 37 | ; DLC handling. 38 | ; Format: = 39 | ; e.g. : 247295 = Saints Row IV - GAT V Pack 40 | ; If the DLC is not specified in this section 41 | ; then it won't be unlocked 42 | SAC_DLC -------------------------------------------------------------------------------- /sac_emu/dlc_creamapi/files/steam_api.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/sac_emu/dlc_creamapi/files/steam_api.dll -------------------------------------------------------------------------------- /sac_emu/dlc_creamapi/files/steam_api64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/sac_emu/dlc_creamapi/files/steam_api64.dll -------------------------------------------------------------------------------- /sac_emu/dlc_creamapi/infos.txt: -------------------------------------------------------------------------------- 1 | The goal of this is to unlock all DLCs for a legit bought Steam game. 2 | 3 | The DLLs included are: CreamAPI v5.1.0.0 non-log build (source: https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576) -------------------------------------------------------------------------------- /sac_emu/game_ali213/files/SteamConfig.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | ; The Game AppID (store.steampowered.com/app/XXXXXXX/) 3 | AppID = SAC_AppID 4 | 5 | ; Source steam_api(64).dll version 6 | API = SAC_APIVersion 7 | 8 | ; Player name you'll have in-game 9 | PlayerName = VALVE 10 | 11 | ; Game Language 12 | ; english german french italian koreana 13 | ; spanish schinese tchinese russian thai 14 | ; japanese portuguese polish danish dutch 15 | ; finnish norwegian swedish hungarian czech 16 | ; romanian turkish 17 | Language = english 18 | 19 | ; Save types: 20 | ; VALVE(game dir) 0 21 | ; VALVE(my documents) 1 22 | ; RELOADED 4 23 | ; SKIDROW 5 24 | ; FLT 6 25 | ; CODEX(3.0.4+/my documents) 7 26 | ; CODEX(1.0.0.0+/APPDATA) 8 27 | SaveType = 0 28 | 29 | ;Achievements Count limit 30 | ;AchievementsCount=0 31 | 32 | ; Game exe path 33 | ;GameEXE=game.exe 34 | 35 | ;SteamUserID = 12345678 36 | ;SteamUserIDH = 12345678 37 | 38 | ;IsLoggedOn=0 39 | ;Online=0 40 | 41 | UnLockListedDLCOnly=1 42 | 43 | [DLC] 44 | ; To unlock a DLC, use this format: "AppID = DLC Name" 45 | SAC_DLC 46 | 47 | [Option] 48 | ; Block Network (internet connections made by the game) 49 | FullBlockNetwork=1 50 | 51 | ; Redirect File Handle to ".valve" 52 | FileRedirectCheck=1 53 | 54 | ; DECRYPT STEAM STUB 55 | ; 1 tells steam_api.dll / steam_api64.dll to decrypt the steam stub as soon as possible 56 | ; 2 tells SteamClient.dll / SteamClient64.dll to decrypt the steam stub as soon as possible 57 | ; 0 to disable it. 58 | DECRYPT_STEAM_STUB=0 -------------------------------------------------------------------------------- /sac_emu/game_ali213/files/steam_api.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/sac_emu/game_ali213/files/steam_api.dll -------------------------------------------------------------------------------- /sac_emu/game_ali213/files/steam_api64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/sac_emu/game_ali213/files/steam_api64.dll -------------------------------------------------------------------------------- /sac_emu/game_ali213/infos.txt: -------------------------------------------------------------------------------- 1 | The goal of this is to crack the game as well as unlock every DLC for an uncracked game. 2 | 3 | The DLLs included are: ALI213 v8.33.9.23 (source: https://cs.rin.ru/forum/viewtopic.php?p=2929542#p2929542) -------------------------------------------------------------------------------- /sac_emu/game_goldberg/files/steam_api.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/sac_emu/game_goldberg/files/steam_api.dll -------------------------------------------------------------------------------- /sac_emu/game_goldberg/files/steam_api64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/d072170d753a5ff492a1500bac8018b1ed2ae6d7/sac_emu/game_goldberg/files/steam_api64.dll -------------------------------------------------------------------------------- /sac_emu/game_goldberg/files/steam_settings/DLC.txt: -------------------------------------------------------------------------------- 1 | SAC_NoSpaceDLC -------------------------------------------------------------------------------- /sac_emu/game_goldberg/files/steam_settings/steam_appid.txt: -------------------------------------------------------------------------------- 1 | SAC_AppID -------------------------------------------------------------------------------- /sac_emu/game_goldberg/infos.txt: -------------------------------------------------------------------------------- 1 | The goal of this is to crack the game as well as unlock every DLC for an uncracked game. 2 | 3 | The DLLs included are: Goldberg (experimental) commit 475342f0 (source: https://mr_goldberg.gitlab.io/goldberg_emulator/) -------------------------------------------------------------------------------- /sac_lib/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sac_lib/get_file_version.py: -------------------------------------------------------------------------------- 1 | import win32api 2 | 3 | def GetFileVersion(filename: str) -> str: 4 | fileInfos = win32api.GetFileVersionInfo(filename, "\\") 5 | return "%d.%d.%d.%d" % (fileInfos['FileVersionMS'] / 65536, fileInfos['FileVersionMS'] % 65536, fileInfos['FileVersionLS'] / 65536, fileInfos['FileVersionLS'] % 65536) -------------------------------------------------------------------------------- /steam_auto_cracker_gui.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | try: # Handles Python errors to write them to a log file so they can be reported and fixed more easily. 4 | import tkinter as tk 5 | from tkinter import ttk, filedialog, font 6 | from tkinterdnd2 import DND_FILES, TkinterDnD 7 | 8 | import requests 9 | import configparser 10 | import json 11 | import os 12 | import subprocess 13 | from sac_lib.get_file_version import GetFileVersion 14 | import shutil 15 | from time import sleep 16 | from sys import exit 17 | 18 | VERSION = "2.2.2" 19 | 20 | RETRY_DELAY = 15 # Delay in seconds before retrying a failed request. (default, can be modified in config.ini) 21 | RETRY_MAX = 30 # Number of failed tries (includes the first try) after which SAC will stop trying and quit. (default, can be modified in config.ini) 22 | 23 | HIGH_DLC_WARNING = 125 24 | 25 | folder_path = "" 26 | appID = 0 27 | gameSearchDone = False 28 | 29 | STATE_FindingInAppList = False 30 | STATE_UpdatingAppList = False 31 | 32 | EXTS_TO_REPLACE = (".txt", ".ini", ".cfg") 33 | 34 | GITHUB_LATESTVERSIONJSON = "https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/autoupdater/latestversion.json" 35 | GITHUB_AUTOUPDATER = "https://raw.githubusercontent.com/BigBoiCJ/SteamAutoCracker/autoupdater/steam_auto_cracker_gui_autoupdater.exe" 36 | 37 | def OnTkinterError(exc, val, tb): 38 | # Handle Tkinter Python errors 39 | print("\n[!!!] A Tkinter Python error occurred! Writing the error to the error_tkinter.log file.\n---") 40 | with open("error_tkinter.log", "w", encoding="utf-8") as errorFile: 41 | errorFile.write(f"SteamAutoCracker GUI v{VERSION}\n---\nA Tkinter Python error occurred!\nPlease report it on GitHub or cs.rin.ru\nMake sure to blank any personal detail.\nNOTE: '_tkinter.TclError: invalid command name' errors are normal if you closed the window while SAC was busy. In that case, you should not report the issue and just ignore it.\n---\n\n") 42 | traceback.print_exc(file=errorFile) 43 | traceback.print_exc() 44 | print("---\nError written to error_tkinter.log, please report it on GitHub or cs.rin.ru\nMake sure to blank any personal detail.") 45 | 46 | try: 47 | update_logs("[!!!] A Tkinter Python error occurred! The error has written to error_tkinter.log, please report it on GitHub or cs.rin.ru\nMake sure to blank any personal detail.") 48 | except Exception: 49 | pass 50 | 51 | class SACRequest: 52 | def __init__(self, url:str, name:str = "Unnamed"): 53 | self.url = url 54 | self.tries = 0 55 | self.name = name 56 | self.DoRequest() 57 | 58 | def DoRequest(self): 59 | self.tries += 1 60 | req = requests.get(self.url, timeout=10) 61 | if not req.ok: 62 | if self.tries < int(config["Advanced"]["RetryMax"]): 63 | # Do another try 64 | update_logs("- " + self.name + " request failed, retrying in " + config["Advanced"]["RetryDelay"] + "s... (" + str(self.tries) + "/" + config["Advanced"]["RetryMax"] + " tries)") 65 | root.update() 66 | sleep(int(config["Advanced"]["RetryDelay"])) 67 | self.DoRequest() 68 | else: 69 | update_logs("[!] Connection failed after " + config["Advanced"]["RetryMax"] + " tries. Are you connected to the Internet? Is Steam online?\nIf you being rate limited (too many DLCs), you should try increasing retrydelay and retrymax in the config") 70 | raise Exception(f"SACRequest: Connection failed after {config['Advanced']['RetryMax']} tries") 71 | else: 72 | self.req = req 73 | 74 | def handle_folder_selection(event=None): 75 | global folder_path 76 | global last_selected_folder 77 | last_selected_folder = config["Preferences"].get("last_selected_folder", "") 78 | # Reset and hide UI elements related to folder selection and game cracking 79 | def reset_folder_selection_ui(): 80 | selectedFolderLabel.config(text="") 81 | selectedFolderLabel.pack_forget() 82 | frameGame2.pack_forget() 83 | frameCrack2.pack_forget() # Hide the crack frame 84 | 85 | # Determine the folder path based on the event type 86 | if event: # Handling drag and drop 87 | folder_path_temp = event.data.strip("{}").replace("\\", "/") # Returns the directory with no "/" at the end 88 | else: # Handling button click 89 | initial_dir = "/" 90 | if last_selected_folder != "" and os.path.isdir(last_selected_folder): 91 | initial_dir = last_selected_folder 92 | folder_path_temp = filedialog.askdirectory(initialdir=initial_dir) # Returns the directory with no "/" at the end 93 | 94 | if os.path.isdir(folder_path_temp): 95 | folder_path = folder_path_temp 96 | # Update the last dropped folder for future use 97 | last_selected_folder = os.path.dirname(folder_path) # If no "/" at the end, returns the parent directory 98 | config["Preferences"]["last_selected_folder"] = last_selected_folder 99 | UpdateConfig() 100 | folder_name = os.path.basename(folder_path) # Gets the name of the folder ("C:/Something/Games/Hello" will return "Hello") 101 | 102 | # Update UI elements with the selected folder information 103 | update_logs(f"\nSelected folder: {folder_path}") 104 | selectedFolderLabel.config(text=f"Selected folder:\n{folder_path}") 105 | selectedFolderLabel.pack() 106 | frameGame2.pack() 107 | 108 | # Update the game name entry with the folder name 109 | gameNameEntry.delete(0, tk.END) # Removes the content of the Entry element starting from index 0 to the end 110 | gameNameEntry.insert(0, folder_name) # Inserts the name of the folder in the Entry element at the start of it (index 0) 111 | 112 | # Show crack frame if game search is done 113 | if gameSearchDone: 114 | frameCrack2.pack() 115 | else: 116 | # Handle invalid folder selection 117 | update_logs("\nNo valid folder selected") 118 | folder_path = "" 119 | reset_folder_selection_ui() 120 | 121 | def update_logs(log_message): 122 | # Get current content 123 | current_logs = logs_text.get("1.0", tk.END) 124 | 125 | logs_text.config(state=tk.NORMAL) # Enables modification (needed to add content) 126 | # Delete the current content 127 | logs_text.delete("1.0", tk.END) 128 | 129 | # Insert the new message at the end with a linebreak 130 | logs_text.insert(tk.END, current_logs + log_message) 131 | 132 | # Scroll the widget to the bottom 133 | logs_text.yview_moveto(1.0) 134 | 135 | # Focus on the end 136 | logs_text.see(tk.END) 137 | logs_text.config(state=tk.DISABLED) # Disables modification (prevents the user from writing inside the field) 138 | 139 | def search_game(): 140 | searchGameButton.config(state=tk.DISABLED) # Prevents the user from starting multiple searches at the same time 141 | frameCrack2.pack_forget() # Hide the crack frame 142 | global gameSearchDone 143 | gameSearchDone = False 144 | 145 | gameFoundStatus.config(text=f"") 146 | # Disable the ability to change the selected folder 147 | selectFolderButton.config(state=tk.DISABLED) 148 | updateAppListButton.grid_forget() 149 | root.update() 150 | 151 | global appID 152 | appID = 0 153 | if gameNameEntry.get() == "": 154 | update_logs("\n[!] Please enter a valid Name or AppID") 155 | searchGameButton.config(state=tk.NORMAL) # Re-enable the ability to search the game 156 | selectFolderButton.config(state=tk.NORMAL) # Re-enable the ability to change the selected folder 157 | return 158 | 159 | try: 160 | appID = int(gameNameEntry.get()) 161 | except: 162 | appID = FindInAppList(gameNameEntry.get()) 163 | 164 | if appID != 0 and RetrieveGame(): # On success 165 | # We are now on step 3 166 | gameSearchDone = True 167 | frameCrack2.pack() # Show the crack frame 168 | searchGameButton.config(state=tk.NORMAL) # Re-enable the ability to search the game 169 | selectFolderButton.config(state=tk.NORMAL) # Re-enable the ability to change the selected folder 170 | else: 171 | searchGameButton.config(state=tk.NORMAL) # Re-enable the ability to search the game 172 | selectFolderButton.config(state=tk.NORMAL) # Re-enable the ability to change the selected folder 173 | 174 | def FindInAppList(appName): 175 | update_logs("\nImporting and searching the App List, this could take a few seconds if your computer isn't powerful enough.") 176 | gameFoundStatus.config(text=f"Searching in the App List...") 177 | root.update() # Update the window now 178 | try: 179 | with open("applist.txt", "r", encoding="utf-8") as file: 180 | data = json.load(file) 181 | except: 182 | update_logs("The App List isn't downloaded on your computer, downloading it...") 183 | UpdateAppList() 184 | return FindInAppList(appName) # Re launch this funtion 185 | 186 | for elem in data["applist"]["apps"]: 187 | if elem["name"].lower() != appName.lower(): 188 | continue 189 | 190 | return elem["appid"] 191 | 192 | update_logs("[!] The App was not found, make sure you entered EXACTLY the Steam Game's name (watch it on Steam)") 193 | update_logs("If you typed it properly, you can try to update the App List. Alternatively, you can try entering the AppID.") 194 | gameFoundStatus.config(text=f"App not found!") 195 | 196 | updateAppListButton.grid(row=0, column=2, padx=(10, 0)) 197 | return 0 198 | 199 | def UpdateAppList(): 200 | updateAppListButton.grid_forget() 201 | update_logs("\nUpdating the App List, this could take a few seconds to up to a minute, depending on your internet connection.") 202 | gameFoundStatus.config(text=f"Updating the App List...") 203 | root.update() 204 | try: 205 | req = SACRequest("https://api.steampowered.com/ISteamApps/GetAppList/v2/", "UpdateAppList").req 206 | except Exception: 207 | gameFoundStatus.config(text=f"An error has occurred") 208 | return 209 | 210 | with open("applist.txt", "w", encoding="utf-8") as file: 211 | file.write(req.text) 212 | update_logs("App List updated!") 213 | gameFoundStatus.config(text=f"App List updated!") 214 | 215 | def RetrieveAppName(appID: int) -> str: 216 | try: 217 | req = SACRequest("https://store.steampowered.com/api/appdetails?appids=" + str(appID) + "&filters=basic", "RetrieveAppName").req 218 | except Exception: 219 | return "error" 220 | 221 | data = req.json() 222 | data = data[str(appID)] 223 | if (not "data" in data) or (not "name" in data["data"]): 224 | return "error" 225 | return data["data"]["name"] 226 | 227 | def RetrieveGame() -> bool: 228 | global appID 229 | global gameName 230 | global dlcIDs 231 | global dlcNames 232 | 233 | dlcIDs = [] 234 | dlcNames = [] 235 | 236 | update_logs("\n[1/2] Retrieving game informations from Steam...") 237 | gameFoundStatus.config(text=f"[1/2] Retrieving game informations from Steam...") 238 | root.update() 239 | # https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI#appdetails 240 | try: 241 | req = SACRequest("https://store.steampowered.com/api/appdetails?appids=" + str(appID) + "&filters=basic", "RetrieveGame").req 242 | except Exception: 243 | gameFoundStatus.config(text=f"An error has occurred") 244 | return False 245 | data = req.json() 246 | data = data[str(appID)] 247 | if not data["success"]: 248 | update_logs(f"\n[!] AppID {appID} not found.") 249 | gameFoundStatus.config(text=f"AppID {appID} not found.") 250 | appID = 0 251 | return False 252 | if config["Advanced"]["BypassGameVerification"] != "1" and data["data"]["type"] != "game": 253 | update_logs(f"\n[!] AppID {appID} is not a game. You can bypass this verification in the Advanced settings.") 254 | gameFoundStatus.config(text=f"AppID {appID} is not a game.") 255 | appID = 0 256 | return False 257 | 258 | gameName = data["data"]["name"] 259 | appID = data["data"]["steam_appid"] 260 | update_logs(f"- Game found! Name: {gameName} - AppID: {appID}") 261 | 262 | update_logs("\n[2/2] Retrieving DLCs...") 263 | gameFoundStatus.config(text=f"[2/2] Retrieving DLCs...") 264 | root.update() 265 | 266 | # Optional config check 267 | option = "0" 268 | try: 269 | option = config["Developer"]["RetrieveDLCOption"] 270 | except: 271 | pass 272 | 273 | if option == "1": 274 | # Old retrieve option 275 | update_logs("Using the old retrieve option (RetrieveDLCOption is set to 1)") 276 | 277 | if "dlc" in data["data"]: 278 | dlcIDs = data["data"]["dlc"] 279 | dlcIDsLen = len(dlcIDs) 280 | 281 | if dlcIDsLen >= HIGH_DLC_WARNING: 282 | update_logs(f"/!\\ WARNING: This game has more than {HIGH_DLC_WARNING} DLCs. Requests may fail due to Steam rate limiting. If it does, just give it time, it'll eventually manage to retrieve all DLCs.") 283 | 284 | # Get DLCs names 285 | for i in range(dlcIDsLen): 286 | appName = RetrieveAppName(dlcIDs[i]) 287 | if appName == "error": 288 | update_logs(f"[!] Error! No App Name found for AppID {dlcIDs[i]}") 289 | gameFoundStatus.config(text=f"[!] Error! No App Name found for AppID {dlcIDs[i]}") 290 | appID = 0 291 | return False 292 | dlcNames.append(appName) 293 | update_logs("- Found DLC " + str(i+1) + "/" + str(dlcIDsLen) + ": " + appName + " (" + str(dlcIDs[i]) + ")") 294 | gameFoundStatus.config(text=f"[2/2] Retrieving DLCs... ({i+1}/{dlcIDsLen})") 295 | root.update() 296 | else: 297 | update_logs("- No DLC found for this game!") 298 | else: 299 | # Default retrieve option 300 | 301 | try: 302 | req2 = SACRequest("https://store.steampowered.com/dlc/" + str(appID) +"/random/ajaxgetfilteredrecommendations/?query&count=10000", "RetrieveDLC").req 303 | except Exception: 304 | gameFoundStatus.config(text=f"An error has occurred") 305 | return False 306 | data2 = req2.json() 307 | if not data2["success"]: 308 | update_logs("[!] Retrieve DLC request failed!") 309 | gameFoundStatus.config(text=f"Retrieve DLC request failed!") 310 | appID = 0 311 | return False 312 | 313 | if data2["total_count"] == 0: 314 | update_logs("- No DLC found for this game!") 315 | else: 316 | if data2["total_count"] >= HIGH_DLC_WARNING: 317 | update_logs(f"/!\\ WARNING: This game has more than {HIGH_DLC_WARNING} DLCs. Requests may fail due to Steam rate limiting. If it does, just give it time, it'll eventually manage to retrieve all DLCs.") 318 | 319 | resultsIndex = 0 320 | 321 | # format: data-ds-appid="1812883" 322 | i = -1 323 | while i + 1 < data2["total_count"]: 324 | i += 1 325 | 326 | resultsStr = "" 327 | resultsIndex = data2["results_html"].find("data-ds-appid=\"", resultsIndex) 328 | resultsIndex += len("data-ds-appid=\"") 329 | 330 | while data2["results_html"][resultsIndex] != "\"": 331 | resultsStr += data2["results_html"][resultsIndex] 332 | resultsIndex += 1 333 | 334 | dlcID = int(resultsStr) 335 | if dlcID in dlcIDs: # data-ds-appid is present 2 times for each AppID currently. This will allow us to not include it if it is already. 336 | i -= 1 337 | continue 338 | dlcIDs.append(int(resultsStr)) 339 | 340 | # Retrieve DLC name 341 | appName = RetrieveAppName(dlcIDs[i]) 342 | if appName == "error": 343 | update_logs(f"[!] Error! No App Name found for AppID {dlcIDs[i]}") 344 | gameFoundStatus.config(text=f"Error! No App Name found for AppID {dlcIDs[i]}") 345 | appID = 0 346 | return False 347 | dlcNames.append(appName) 348 | update_logs("- Found DLC " + str(i+1) + "/" + str(data2["total_count"]) + ": " + appName + " (" + str(dlcIDs[i]) + ")") 349 | gameFoundStatus.config(text=f"[2/2] Retrieving DLCs... ({i+1}/{data2['total_count']})") 350 | root.update() 351 | 352 | update_logs(f"Finished retrieving all the details about the game {gameName} (appID: {appID})") 353 | gameFoundStatus.config(text=f"All details retrieved for {gameName}!") 354 | return True # Retrieved game and DLCs successfully 355 | 356 | def CrackGame(): 357 | global appID 358 | 359 | # Prevents the user from searching a game or selecting a folder or re-clicking the crack game button 360 | selectFolderButton.config(state=tk.DISABLED) 361 | searchGameButton.config(state=tk.DISABLED) 362 | selectCrackButton.config(state=tk.DISABLED) 363 | crackGameButton.config(state=tk.DISABLED) 364 | 365 | update_logs("\nSearching Steam API DLLs and cracking them...") 366 | cracked = False 367 | 368 | if config["Crack"]["SelectedCrack"][:3] == "dlc" and len(dlcIDs) == 0: # If a dlc only crack has been selected, but the game has no DLC 369 | update_logs("-----\nNo DLC is available, and you selected a DLC only crack. Aborting the cracking process.") 370 | EndCrack() 371 | return 372 | 373 | configDir = os.path.join(os.getcwd(), "sac_emu\\" + config["Crack"]["SelectedCrack"]) # "sac_emu/game_ali213" for example 374 | try: 375 | config.read(configDir + "\\config_override.ini") 376 | except Exception: 377 | pass 378 | 379 | configDir = os.path.join(configDir, "files") # "sac_emu/game_ali213/files" for example 380 | 381 | # Check if some custom Steamless options have been set up 382 | steamlessOptions = "" 383 | try: 384 | steamlessOptions = config["Developer"]["SteamlessOptions"] + " " 385 | except: 386 | pass 387 | 388 | root.update() 389 | 390 | dllLocations = [] 391 | for root_dir, dirs, files in os.walk(folder_path): 392 | apiFile = "" 393 | 394 | # Use Steamless if configured 395 | if config["Preferences"]["Steamless"] == "1" and crackListSteamless[config["Crack"]["SelectedCrack"]]: 396 | # Run Steamless on every .exe file. If it's not under DRM or not the wrong file, no problem! 397 | for fileName in files: 398 | if not fileName.endswith(".exe"): 399 | continue 400 | update_logs(f"- Attempting to run Steamless on {fileName}") 401 | root.update() 402 | #update_logs("\n[[[ Steamless logs ]]]") 403 | fileLocation = root_dir + "/" + fileName 404 | shutil.move(fileLocation, fileName) # Move the file to our location 405 | subprocess.call("Steamless_CLI\\Steamless.CLI.exe " + steamlessOptions + "\"" + fileName + "\"", shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE) # Run Steamless on the game 406 | #update_logs("[[[ -------------- ]]]\n") 407 | 408 | # Check if the game was NOT unpacked 409 | if not os.path.isfile(fileName + ".unpacked.exe"): 410 | # Move back the original game's exe since it didn't change 411 | update_logs("- Couldn't run Steamless on " + fileName + ", it is probably not under DRM.") 412 | shutil.move(fileName, fileLocation) 413 | root.update() 414 | continue 415 | 416 | update_logs(f"- Removed Steam Stub DRM from {fileName}") 417 | if config["FileNames"]["GameEXE"] != "": 418 | # Rename and move back the original game's exe 419 | shutil.move(fileName, fileLocation + config["FileNames"]["GameEXE"]) 420 | else: 421 | # Delete the original game's exe 422 | os.remove(fileName) 423 | # Rename and move the unpacked exe to the game's directory 424 | shutil.move(fileName + ".unpacked.exe", fileLocation) 425 | root.update() 426 | 427 | if "steam_api.dll" in files: 428 | if config["FileNames"]["SteamAPI"] in files: 429 | update_logs("[!] Seems like a file named " + config["FileNames"]["SteamAPI"] + " is present. This could indicate that steam_api.dll has already been cracked! Overwriting steam_api.dll. No backup of the previous steam_api.dll could be created, and the file has been deleted. " + config["FileNames"]["SteamAPI"] + " has been restored.") 430 | os.remove(root_dir + "/steam_api.dll") 431 | shutil.move(root_dir + "/" + config["FileNames"]["SteamAPI"], root_dir + "/steam_api.dll") 432 | 433 | apiFile = root_dir + "/steam_api.dll" 434 | try: 435 | apiFileVersion = GetFileVersion(apiFile) 436 | except Exception: 437 | update_logs("[!] steam_api.dll: could not retrieve the file version! Seems like the steam_api.dll file has already been cracked! Aborting...") 438 | EndCrack() 439 | return 440 | 441 | update_logs(f"- Found steam_api.dll in {root_dir}, planning crack application") 442 | 443 | if "steam_api64.dll" in files: 444 | if config["FileNames"]["SteamAPI64"] in files: 445 | update_logs("[!] Seems like a file named " + config["FileNames"]["SteamAPI64"] + " is present. This could indicate that steam_api64.dll has already been cracked! Overwriting steam_api64.dll. No backup of the previous steam_api64.dll could be created, and the file has been deleted. " + config["FileNames"]["SteamAPI64"] + " has been restored.") 446 | os.remove(root_dir + "/steam_api64.dll") 447 | shutil.move(root_dir + "/" + config["FileNames"]["SteamAPI64"], root_dir + "/steam_api64.dll") 448 | 449 | apiFile = root_dir + "/steam_api64.dll" 450 | try: 451 | apiFileVersion = GetFileVersion(apiFile) 452 | except Exception: 453 | update_logs("[!] steam_api64.dll: could not retrieve the file version! Seems like the steam_api64.dll file has already been cracked! Aborting...") 454 | EndCrack() 455 | return 456 | 457 | update_logs(f"- Found steam_api64.dll in {root_dir}, planning crack application") 458 | 459 | if apiFile != "": 460 | if root_dir not in dllLocations: 461 | dllLocations.append(root_dir) 462 | 463 | cracked = True 464 | root.update() 465 | 466 | for dllCurrentLocation in dllLocations: 467 | for root_dir, dirs, files in os.walk(configDir): 468 | relativeRootDir = root_dir[len(configDir) + 1:] 469 | dllAbsoluteRelativeLocation = os.path.join(dllCurrentLocation, relativeRootDir) 470 | 471 | # To make it look right, add a "\" at the end of relativeRootDir if it is not empty 472 | if len(relativeRootDir) > 0: 473 | relativeRootDir += "\\" 474 | 475 | # Create all missing directories 476 | for dir in dirs: 477 | if not os.path.isdir(os.path.join(dllAbsoluteRelativeLocation, dir)): 478 | os.mkdir(os.path.join(dllAbsoluteRelativeLocation, dir)) 479 | update_logs("Created new directory " + relativeRootDir + dir) 480 | root.update() 481 | 482 | # Create all files 483 | for fileName in files: 484 | root.update() 485 | if os.path.isfile(os.path.join(dllAbsoluteRelativeLocation, fileName)): # The file already exists in the game, rename it to .bak 486 | newName = fileName + config["FileNames"]["BakSuffix"] 487 | if fileName == "steam_api.dll" or fileName == "steam_api64.dll": 488 | if config["Preferences"]["CrackOption"] != "0": # Only create config 489 | update_logs("Ignoring " + relativeRootDir + fileName + " because of the set crack approach") 490 | continue 491 | 492 | if fileName == "steam_api.dll": 493 | newName = config["FileNames"]["SteamAPI"] 494 | else: 495 | newName = config["FileNames"]["SteamAPI64"] 496 | 497 | if newName == "": # Don't keep a backup of the steam_api(64).dll file 498 | os.remove(os.path.join(dllAbsoluteRelativeLocation, fileName)) 499 | update_logs("Removed old " + relativeRootDir + fileName + " file because no backup file name is set") 500 | elif os.path.isfile(os.path.join(dllAbsoluteRelativeLocation, newName)): # A backup of this file already exists, the game might already be cracked, abort! 501 | update_logs("[!] Seems like the backup of " + relativeRootDir + fileName + " file already exists! This could indicate that the game has already been cracked. Overwriting it. No backup of " + relativeRootDir + fileName + " could be created, and the file has been deleted.") 502 | os.remove(os.path.join(dllAbsoluteRelativeLocation, fileName)) 503 | else: 504 | shutil.move(os.path.join(dllAbsoluteRelativeLocation, fileName), os.path.join(dllAbsoluteRelativeLocation, newName)) 505 | update_logs("Backupped old file " + relativeRootDir + fileName + " -> " + newName) 506 | elif fileName == "steam_api.dll" or fileName == "steam_api64.dll": # No existing file, and this file is the steam_api(64).dll one 507 | continue # Ignore this file 508 | 509 | shutil.copyfile(os.path.join(root_dir, fileName), os.path.join(dllAbsoluteRelativeLocation, fileName)) 510 | 511 | # Check if ends with a specific extension, so we can replace the presets inside 512 | if any(fileName.endswith(extension) for extension in EXTS_TO_REPLACE): 513 | # Read the file's content 514 | with open(os.path.join(dllAbsoluteRelativeLocation, fileName), "r", encoding="utf-8") as file: 515 | fileContent = file.read() 516 | 517 | # Replace the presets if any 518 | fileContent = fileContent.replace("SAC_AppID", str(appID)) 519 | fileContent = fileContent.replace("SAC_APIVersion", apiFileVersion) 520 | buffer = "" 521 | for i in range(len(dlcIDs)): 522 | buffer += str(dlcIDs[i]) + " = " + dlcNames[i] + "\n" 523 | fileContent = fileContent.replace("SAC_DLC", buffer) 524 | buffer = "" 525 | for i in range(len(dlcIDs)): 526 | buffer += str(dlcIDs[i]) + "=" + dlcNames[i] + "\n" 527 | fileContent = fileContent.replace("SAC_NoSpaceDLC", buffer) 528 | 529 | # Write the changes 530 | with open(os.path.join(dllAbsoluteRelativeLocation, fileName), "w", encoding="utf-8") as file: 531 | file.write(fileContent) 532 | 533 | update_logs("Created new file " + relativeRootDir + fileName) 534 | 535 | 536 | update_logs("\n-----\nFinished cracking the game!") 537 | if not cracked: 538 | update_logs("[!] No Steam API DLL was found in the game!") 539 | else: 540 | update_logs("The game has been cracked successfully! (If you attempt to crack if again, SAC will try its best to make it work, but will let some leftovers of old cracks.)") 541 | 542 | EndCrack() 543 | 544 | def EndCrack(): 545 | # Cracking process done! 546 | ReloadConfig() # Reload the config to remove the overwritten config from config_override.ini 547 | 548 | # Now let's remove locks 549 | selectFolderButton.config(state=tk.NORMAL) 550 | searchGameButton.config(state=tk.NORMAL) 551 | selectCrackButton.config(state=tk.NORMAL) 552 | crackGameButton.config(state=tk.NORMAL) 553 | 554 | # ----- Settings ----- 555 | 556 | def SettingsButton(): 557 | top = tk.Toplevel(root) 558 | #top.geometry("750x250") 559 | top.title(f"SteamAutoCracker GUI v{VERSION} - Settings") 560 | top.resizable(False, False) # Prevents resizing the window's width and height 561 | biggerFont = DEFAULT_FONT.copy() 562 | biggerFont.config(size=10) 563 | ttk.Label(top, text= "Settings", font=FONT2).pack(padx=200, pady=(10,10), anchor="center") 564 | 565 | ttk.Button(top, text="Reset to default", padding=0, command=ResetSettingsButton).pack(pady=(0,0), anchor="center") 566 | 567 | # Handle scrolling 568 | scrollCanvas = tk.Canvas(top, width=600, height=450, highlightthickness=0) 569 | scrollCanvas.pack(pady=(5, 0), side=tk.LEFT, fill=tk.BOTH, expand=True) 570 | 571 | scrollFrame = ttk.Frame(scrollCanvas) 572 | scrollCanvas.create_window((0,0), window=scrollFrame, anchor="nw") 573 | 574 | # Damn don't ask me how all of this works. It just does :D 575 | scrollbar = tk.Scrollbar(top, command=scrollCanvas.yview) 576 | scrollbar.pack(side=tk.RIGHT, fill=tk.Y) 577 | scrollCanvas.config(yscrollcommand=scrollbar.set) 578 | 579 | def on_mousewheel(event): 580 | scrollCanvas.yview_scroll(int(-1*(event.delta/120)), "units") # Some magic I guess. Huge thanks to LLM who probably stole this code from someone. 581 | 582 | top.bind("", on_mousewheel) 583 | 584 | def configure_canvas(event): 585 | scrollCanvas.config(scrollregion=scrollCanvas.bbox("all")) 586 | 587 | scrollFrame.bind("", configure_canvas) 588 | # Finished handling scrolling 589 | 590 | # Update options (UpdateOption) 591 | ttk.Label(scrollFrame, text="Updates:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") 592 | ttk.Label(scrollFrame, text="This will search the latest version on GitHub.\nIf you're afraid of leaking your IP to GitHub, use a VPN and/or disable auto updating.", font=FONT4, padding=0, foreground="#575757", wraplength=600).pack(padx=(6, 0), pady=(0,0), anchor="w") 593 | settings_frame_updates = ttk.Frame(scrollFrame) 594 | settings_frame_updates.pack(padx=(15, 0), pady=(0, 0), anchor="w") 595 | 596 | ## Radio 597 | global UpdateOption_var 598 | UpdateOption_var = tk.StringVar() 599 | UpdateOption_var.set(config["Preferences"]["UpdateOption"]) 600 | ttk.Radiobutton(settings_frame_updates, text="Don't automatically check for updates (RECOMMENDED FOR PRIVACY)", variable=UpdateOption_var, value="0", command=lambda: UpdateConfigKey("Preferences", "UpdateOption", UpdateOption_var.get())).grid(row=0, column=0, sticky="w") 601 | ttk.Radiobutton(settings_frame_updates, text="Automatically check for updates on SAC start (RECOMMENDED FOR CONVENIENCE)", variable=UpdateOption_var, value="1", command=lambda: UpdateConfigKey("Preferences", "UpdateOption", UpdateOption_var.get())).grid(row=1, column=0, sticky="w") 602 | 603 | # Crack approach (CrackOption) 604 | ttk.Label(scrollFrame, text="Crack approach:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") 605 | settings_frame1 = ttk.Frame(scrollFrame) 606 | settings_frame1.pack(padx=(15, 0), pady=(0, 0), anchor="w") 607 | 608 | ## Radio 609 | global CrackOption_var 610 | CrackOption_var = tk.StringVar() 611 | CrackOption_var.set(config["Preferences"]["CrackOption"]) 612 | ttk.Radiobutton(settings_frame1, text="Crack the game automatically (RECOMMENDED)", variable=CrackOption_var, value="0", command=lambda: UpdateConfigKey("Preferences", "CrackOption", CrackOption_var.get())).grid(row=0, column=0, sticky="w") 613 | ttk.Radiobutton(settings_frame1, text="Only create the crack config, and put it in the same directory as steam_api(64).dll", variable=CrackOption_var, value="1", command=lambda: UpdateConfigKey("Preferences", "CrackOption", CrackOption_var.get())).grid(row=1, column=0, sticky="w") 614 | ttk.Radiobutton(settings_frame1, text="Only create the crack config, and put it in the same directory as the Steam Auto Cracker tool\n(currently bugged, doesn't work!)", variable=CrackOption_var, value="2", command=lambda: UpdateConfigKey("Preferences", "CrackOption", CrackOption_var.get())).grid(row=2, column=0, sticky="w") 615 | 616 | # Steamless (Steamless) 617 | ttk.Label(scrollFrame, text="Steamless:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") 618 | ttk.Label(scrollFrame, text="This will allow SAC to bypass the SteamStub DRM if it is used.", font=FONT4, padding=0, foreground="#575757", wraplength=600).pack(padx=(6, 0), pady=(0,0), anchor="w") 619 | 620 | settings_frame2 = ttk.Frame(scrollFrame) 621 | settings_frame2.pack(padx=(15, 0), pady=(0, 10), anchor="w") 622 | 623 | ## Radio 624 | global Steamless_var 625 | Steamless_var = tk.StringVar() 626 | Steamless_var.set(config["Preferences"]["Steamless"]) 627 | ttk.Radiobutton(settings_frame2, text="Don't attempt to use Steamless", variable=Steamless_var, value="0", command=lambda: UpdateConfigKey("Preferences", "Steamless", Steamless_var.get())).grid(row=0, column=0, sticky="w") 628 | ttk.Radiobutton(settings_frame2, text="Attempt to use Steamless (RECOMMENDED)", variable=Steamless_var, value="1", command=lambda: UpdateConfigKey("Preferences", "Steamless", Steamless_var.get())).grid(row=1, column=0, sticky="w") 629 | 630 | # FileNames 631 | ttk.Label(scrollFrame, text="File names:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") 632 | ttk.Label(scrollFrame, text="You can enter the name the different files will have.\nIt is recommended to keep the default ones.", font=FONT4, padding=0, foreground="#575757", wraplength=600).pack(padx=(6, 0), pady=(0,0), anchor="w") 633 | 634 | fileNamesFrame = ttk.Frame(scrollFrame) 635 | fileNamesFrame.pack(padx=(15, 0), pady=(0, 10), anchor="w") 636 | 637 | tk.Label(fileNamesFrame, text="steam_api.dll backup name:").grid(row=0, column=0) 638 | global SteamApi_var 639 | SteamApi_var = tk.StringVar() 640 | steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=SteamApi_var) 641 | steamApiEntry.grid(row=0, column=1, ipadx=10, ipady=3) 642 | SteamApi_var.set(config["FileNames"]["SteamAPI"]) 643 | ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("SteamAPI", SteamApi_var)).grid(row=0, column=2, ipadx=10) 644 | 645 | tk.Label(fileNamesFrame, text="steam_api64.dll backup name:").grid(row=1, column=0) 646 | global SteamApi64_var 647 | SteamApi64_var = tk.StringVar() 648 | steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=SteamApi64_var) 649 | steamApiEntry.grid(row=1, column=1, ipadx=10, ipady=3) 650 | SteamApi64_var.set(config["FileNames"]["SteamAPI64"]) 651 | ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("SteamAPI64", SteamApi64_var)).grid(row=1, column=2, ipadx=10) 652 | 653 | tk.Label(fileNamesFrame, text="Game EXE backup suffix:").grid(row=2, column=0) 654 | global GameEXE_var 655 | GameEXE_var = tk.StringVar() 656 | steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=GameEXE_var) 657 | steamApiEntry.grid(row=2, column=1, ipadx=10, ipady=3) 658 | GameEXE_var.set(config["FileNames"]["GameEXE"]) 659 | ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("GameEXE", GameEXE_var)).grid(row=2, column=2, ipadx=10) 660 | 661 | tk.Label(fileNamesFrame, text="Other files backup suffix:").grid(row=3, column=0) 662 | global BakSuffix_var 663 | BakSuffix_var = tk.StringVar() 664 | steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=BakSuffix_var) 665 | steamApiEntry.grid(row=3, column=1, ipadx=10, ipady=3) 666 | BakSuffix_var.set(config["FileNames"]["BakSuffix"]) 667 | ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("BakSuffix", BakSuffix_var)).grid(row=3, column=2, ipadx=10) 668 | 669 | # Advanced 670 | ttk.Label(scrollFrame, text="Advanced:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") 671 | ttk.Label(scrollFrame, text="Advanced settings, don't modify unless you know what you're doing.", font=FONT4, padding=0, foreground="#575757", wraplength=600).pack(padx=(6, 0), pady=(0,0), anchor="w") 672 | 673 | advTextFrame = ttk.Frame(scrollFrame) 674 | advTextFrame.pack(padx=(15, 0), pady=(0, 10), anchor="w") 675 | 676 | tk.Label(advTextFrame, text="RetryDelay:").grid(row=0, column=0) 677 | global RetryDelay_var 678 | RetryDelay_var = tk.StringVar() 679 | steamApiEntry = tk.Entry(advTextFrame, width=35, textvariable=RetryDelay_var) 680 | steamApiEntry.grid(row=0, column=1, ipadx=10, ipady=3) 681 | RetryDelay_var.set(config["Advanced"]["RetryDelay"]) 682 | ttk.Button(advTextFrame, text="Save", padding=3, command=lambda: UpdateAdvanced("RetryDelay", RetryDelay_var)).grid(row=0, column=2, ipadx=10) 683 | 684 | tk.Label(advTextFrame, text="RetryMax:").grid(row=1, column=0) 685 | global RetryMax_var 686 | RetryMax_var = tk.StringVar() 687 | steamApiEntry = tk.Entry(advTextFrame, width=35, textvariable=RetryMax_var) 688 | steamApiEntry.grid(row=1, column=1, ipadx=10, ipady=3) 689 | RetryMax_var.set(config["Advanced"]["RetryMax"]) 690 | ttk.Button(advTextFrame, text="Save", padding=3, command=lambda: UpdateAdvanced("RetryMax", RetryMax_var)).grid(row=1, column=2, ipadx=10) 691 | 692 | global BypassGameVerification_var 693 | BypassGameVerification_var = tk.StringVar() 694 | BypassGameVerification_var.set(config["Advanced"]["BypassGameVerification"]) 695 | advBypassGameVerification = ttk.Checkbutton(scrollFrame, text="Bypass the game verification, allows to crack AppIDs not recognized as games", variable=BypassGameVerification_var, command=lambda: UpdateAdvanced("BypassGameVerification", BypassGameVerification_var)) 696 | advBypassGameVerification.pack(padx=(15, 0), pady=(0, 10), anchor="w") 697 | 698 | top.grab_set() # Catches all interactions, prevents the user from interacting with the root window 699 | 700 | def UpdateFileName(key, strVar): 701 | value = strVar.get().strip() 702 | strVar.set(value) 703 | UpdateConfigKey("FileNames", key, value) 704 | 705 | def UpdateAdvanced(key, strVar): 706 | value = strVar.get().strip() 707 | try: 708 | int(value) 709 | except: 710 | strVar.set(config["Advanced"][key]) 711 | else: # If no error 712 | strVar.set(value) 713 | UpdateConfigKey("Advanced", key, value) 714 | 715 | def ResetSettingsButton(): 716 | ResetConfig(1) 717 | 718 | # Update the radio buttons values 719 | UpdateOption_var.set(config["Preferences"]["UpdateOption"]) 720 | CrackOption_var.set(config["Preferences"]["CrackOption"]) 721 | Steamless_var.set(config["Preferences"]["Steamless"]) 722 | SteamApi_var.set(config["FileNames"]["SteamAPI"]) 723 | SteamApi64_var.set(config["FileNames"]["SteamAPI64"]) 724 | GameEXE_var.set(config["FileNames"]["GameEXE"]) 725 | BakSuffix_var.set(config["FileNames"]["BakSuffix"]) 726 | RetryDelay_var.set(config["Advanced"]["RetryDelay"]) 727 | RetryMax_var.set(config["Advanced"]["RetryMax"]) 728 | BypassGameVerification_var.set(config["Advanced"]["BypassGameVerification"]) 729 | 730 | # ----- Crack List ----- 731 | 732 | crackList = { # A list of all selectable cracks 733 | "game_ali213": ["ALI213 (Game)", "The ALI213 crack is simple and can crack a full game. It will unlock all DLCs and will also prevent the game from connecting to the internet.\nThe game folder can then freely be shared with others as the crack is contained inside the game folder.\nIf it doesn't work, consider using Goldberg instead."], 734 | "game_goldberg": ["Goldberg (Game)", "The Goldberg (experimental) crack is similar to ALI213's one.\nIt is open-source, which is better, but might not work with older games, due to SAC's current partial support.\nThis crack will however work better for recent games, where ALI213 could fail.\nInternet connection is blocked, but LAN is enabled."], 735 | "dlc_creamapi": ["CreamAPI (DLC)", "The CreamAPI crack will unlock all DLCs but will not crack the main game. It is meant to be used with bought copies of a game, with your real Steam account.\nOnly use this is you have purchased the game on Steam and want to unlock its DLCs.\nWill not work for most online games, but might exceptionally work with some like Beat Saber."] 736 | } 737 | 738 | crackListSteamless = { # Whether to use Steamless with a specific crack. True = use Steamless 739 | "game_ali213": True, 740 | "game_goldberg": True, 741 | "dlc_creamapi": False 742 | } 743 | 744 | def DisplayCrackList(): 745 | top = tk.Toplevel(root) 746 | top.title(f"SteamAutoCracker GUI v{VERSION} - Crack List") 747 | top.resizable(False, False) # Prevents resizing the window's width and height 748 | biggerFont = DEFAULT_FONT.copy() 749 | biggerFont.config(size=10) 750 | ttk.Label(top, text= "Crack List", font=FONT2).pack(padx=200, pady=(10,10), anchor="center") 751 | 752 | ttk.Button(top, text="Reset to default", padding=0, command=ResetCrackListButton).pack(pady=(0,0), anchor="center") 753 | 754 | # Selected crack (SelectedCrack) 755 | ttk.Label(top, text="Selected crack:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") 756 | settings_frame1 = ttk.Frame(top) 757 | settings_frame1.pack(padx=(15, 0), pady=(0, 0), anchor="w") 758 | 759 | ## Radio 760 | global SelectedCrack_var 761 | SelectedCrack_var = tk.StringVar() 762 | SelectedCrack_var.set(config["Crack"]["SelectedCrack"]) 763 | rowNum = 0 764 | for k, v in crackList.items(): 765 | ttk.Radiobutton(settings_frame1, text=v[0], variable=SelectedCrack_var, value=k, command=lambda: UpdateSelectedCrack()).grid(row=rowNum, column=0, sticky="w") 766 | rowNum += 1 767 | if len(v) > 1: # Contains a description 768 | tk.Label(settings_frame1, text=v[1], font=FONT4, foreground="#575757", wraplength=700, justify="left").grid(row=rowNum, column=0, sticky="w", ipadx=20) 769 | rowNum += 1 770 | 771 | # Spacer 772 | tk.Label(top, text="").pack() 773 | 774 | top.grab_set() # Catches all interactions, prevents the user from interacting with the root window 775 | 776 | def UpdateSelectedCrack(): 777 | value = SelectedCrack_var.get() 778 | UpdateConfigKey("Crack", "SelectedCrack", value) 779 | UpdateSelectedCrackDisplay() 780 | 781 | def UpdateSelectedCrackDisplay(): 782 | selectCrackButton.config(text=crackList[config["Crack"]["SelectedCrack"]][0]) # Display the name of the selected crack on the select crack button in the root window 783 | 784 | def ResetCrackListButton(): 785 | ResetConfig(2) 786 | 787 | # Update the radio buttons values 788 | SelectedCrack_var.set(config["Crack"]["SelectedCrack"]) 789 | 790 | # Update the root button's text 791 | UpdateSelectedCrackDisplay() 792 | 793 | # --------------------------------------- 794 | 795 | def UpdateConfig(): 796 | with open("config.ini", "w", encoding="utf-8") as configFile: 797 | config.write(configFile) 798 | 799 | def UpdateConfigKey(section: str, key: str, value: str): 800 | config[section][key] = value 801 | UpdateConfig() 802 | 803 | def ResetConfig(resetLevel = 0, customConfig=None): 804 | """resetLevel values: 805 | 0 = Everything 806 | 1 = Main settings only (Preferences, FileNames, Advanced) 807 | 2 = Crack selection settings only (Crack) 808 | """ 809 | if customConfig: 810 | currentConfig = customConfig 811 | else: 812 | currentConfig = config 813 | 814 | if resetLevel == 0 or resetLevel == 1: 815 | currentConfig["Preferences"] = {} 816 | currentConfig["Preferences"]["UpdateOption"] = "0" 817 | currentConfig["Preferences"]["CrackOption"] = "0" 818 | currentConfig["Preferences"]["Steamless"] = "1" 819 | currentConfig["Preferences"]["last_selected_folder"] = "" 820 | 821 | currentConfig["FileNames"] = {} 822 | currentConfig["FileNames"]["GameEXE"] = ".bak" 823 | currentConfig["FileNames"]["BakSuffix"] = ".bak" 824 | currentConfig["FileNames"]["SteamAPI"] = "steam_api.dll.bak" 825 | currentConfig["FileNames"]["SteamAPI64"] = "steam_api64.dll.bak" 826 | 827 | currentConfig["Advanced"] = {} 828 | currentConfig["Advanced"]["RetryDelay"] = str(RETRY_DELAY) 829 | currentConfig["Advanced"]["RetryMax"] = str(RETRY_MAX) 830 | currentConfig["Advanced"]["BypassGameVerification"] = "0" 831 | if resetLevel == 0 or resetLevel == 2: 832 | currentConfig["Crack"] = {} 833 | currentConfig["Crack"]["SelectedCrack"] = "game_ali213" 834 | 835 | if not customConfig: 836 | UpdateConfig() 837 | 838 | def FillConfig(currentConfig, configDefault): 839 | changed = False 840 | for k, v in configDefault.items(): 841 | if k not in currentConfig: 842 | currentConfig[k] = v 843 | print("Updated", k, "->", v) 844 | changed = True 845 | if type(v) == configparser.SectionProxy: 846 | if FillConfig(currentConfig[k], v): 847 | changed = True 848 | 849 | return changed 850 | 851 | def ReloadConfig(): 852 | global config 853 | config = configparser.ConfigParser() 854 | 855 | if config.read("config.ini") == []: 856 | # Config doesn't exist, create it 857 | ResetConfig() 858 | else: 859 | # Create a config with default values 860 | configDefault = configparser.ConfigParser() 861 | ResetConfig(0, configDefault) 862 | 863 | # Check if the config is complete. If not, complete it. 864 | changed = FillConfig(config, configDefault) 865 | if changed: 866 | print("[SAC] config.ini has been updated, missing entries have been created") 867 | UpdateConfig() 868 | 869 | ReloadConfig() 870 | 871 | # --------------------------------------- 872 | 873 | def CheckUpdates(): 874 | updatesButton.config(text="Searching for updates...", state=tk.DISABLED) 875 | root.update() 876 | 877 | req = SACRequest(GITHUB_LATESTVERSIONJSON, "RetrieveLatestVersionJson").req 878 | data = req.json() 879 | global latestversion 880 | latestversion = data["version"] 881 | if latestversion == VERSION: # The latest stable version is the one we're running 882 | updatesButton.config(text="SAC is up to date!", state=tk.NORMAL) 883 | return 884 | 885 | global release_link 886 | release_link = data["release"] 887 | release_link = release_link.replace("[VERSION]", latestversion) 888 | 889 | updatesButton.config(text="SAC is outdated!", state=tk.NORMAL) 890 | DisplayUpdate() 891 | 892 | def DisplayUpdate(): 893 | top = tk.Toplevel(root) 894 | top.title(f"SteamAutoCracker GUI v{VERSION} - Update") 895 | top.resizable(False, False) # Prevents resizing the window's width and height 896 | biggerFont = DEFAULT_FONT.copy() 897 | biggerFont.config(size=10) 898 | ttk.Label(top, text= "Update", font=FONT2).pack(padx=200, pady=(10,10), anchor="center") 899 | ttk.Label(top, text="A new update for Steam Auto Cracker GUI is available.\nDo you want to download it automatically?", font=biggerFont, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") 900 | ttk.Label(top, text=f"Current version: {VERSION}", font=biggerFont, padding=0).pack(padx=(6, 0), pady=(15,0), anchor="w") 901 | ttk.Label(top, text=f"Latest version: {latestversion}", font=biggerFont, padding=0).pack(padx=(6, 0), pady=(0,10), anchor="w") 902 | 903 | updateDisplayButtonsFrame = ttk.Frame(top) 904 | updateDisplayButtonsFrame.pack(pady=(5,20)) 905 | 906 | global updateDisplayButtonUpdate 907 | updateDisplayButtonUpdate = ttk.Button(updateDisplayButtonsFrame, text="Update now", command=UpdateSAC, padding=3) 908 | updateDisplayButtonUpdate.grid(row=0, column=0) 909 | 910 | global updateDisplayButtonCopy 911 | updateDisplayButtonCopy = ttk.Button(updateDisplayButtonsFrame, text="Copy the release URL", command=CopyReleaseURL, padding=3) 912 | updateDisplayButtonCopy.grid(row=0, column=1, padx=(50,0)) 913 | 914 | global updateDisplayButtonClose 915 | updateDisplayButtonClose = ttk.Button(updateDisplayButtonsFrame, text="Don't update yet", command=top.destroy, padding=3) 916 | updateDisplayButtonClose.grid(row=0, column=2, padx=(50,0)) 917 | 918 | global updateDisplayStatusLabel 919 | updateDisplayStatusLabel = ttk.Label(top, text="", font=biggerFont, padding=0) 920 | 921 | top.grab_set() # Catches all interactions, prevents the user from interacting with the root window 922 | 923 | global updateDisplayTop 924 | updateDisplayTop = top 925 | 926 | def UpdateSAC(): 927 | updateDisplayButtonUpdate.config(state=tk.DISABLED) 928 | updateDisplayButtonCopy.config(state=tk.DISABLED) 929 | updateDisplayButtonClose.config(state=tk.DISABLED) 930 | 931 | updateDisplayStatusLabel.pack(pady=(0,20), anchor="center") 932 | updateDisplayStatusLabel.config(text="Downloading the autoupdater, please wait...\nThis might take some time depending on your internet connection speed...") 933 | root.update() 934 | 935 | # Check for the existence of a leftover autoupdater 936 | if os.path.isfile("steam_auto_cracker_gui_autoupdater.exe"): 937 | try: 938 | os.remove("steam_auto_cracker_gui_autoupdater.exe") 939 | except Exception: # In case the file is locked for example 940 | updateDisplayButtonUpdate.config(state=tk.NORMAL) 941 | updateDisplayButtonCopy.config(state=tk.NORMAL) 942 | updateDisplayButtonClose.config(state=tk.NORMAL) 943 | updateDisplayStatusLabel.config(text="An error occurred. The autoupdater (steam_auto_cracker_gui_autoupdater.exe) already exists.\nSAC couldn't delete the autoupdater. Please try to remove it yourself, or try again.") 944 | root.update() 945 | return 946 | print("Removed leftover autoupdater") 947 | 948 | # Override RetryDelay and RetryMax 949 | config["Advanced"]["RetryDelay"] = "3" 950 | config["Advanced"]["RetryMax"] = "5" 951 | 952 | req = SACRequest(GITHUB_AUTOUPDATER, "DownloadAutoupdater").req 953 | 954 | updateDisplayStatusLabel.config(text="Writing the autoupdater, please wait...") 955 | root.update() 956 | 957 | with open("steam_auto_cracker_gui_autoupdater.exe", mode="wb") as file: 958 | file.write(req.content) 959 | 960 | updateDisplayStatusLabel.config(text="Autoupdater installed!\nStarting it in 3 seconds...") 961 | root.update() 962 | 963 | sleep(3) 964 | subprocess.Popen("steam_auto_cracker_gui_autoupdater.exe") # Open SAC GUI Autoupdater 965 | exit() 966 | 967 | def CopyReleaseURL(): 968 | root.clipboard_clear() 969 | root.clipboard_append(release_link) 970 | 971 | # --------------------------------------- 972 | 973 | 974 | # Let's now create the main window 975 | root = TkinterDnD.Tk() 976 | root.resizable(False, False) # Prevents resizing the window's width and height 977 | root.title(f"SteamAutoCracker GUI v{VERSION}") 978 | root.drop_target_register(DND_FILES) # Register the drop target 979 | root.dnd_bind("<>", lambda event: handle_folder_selection(event=event)) # Bind the drop target 980 | 981 | DEFAULT_FONT = font.nametofont('TkTextFont') 982 | FONT2 = DEFAULT_FONT.copy() 983 | FONT2.config(size=15) 984 | FONT3 = DEFAULT_FONT.copy() 985 | FONT3.config(size=12) 986 | FONT4 = DEFAULT_FONT.copy() 987 | FONT4.config(size=8) 988 | FONT_APP_ENTRY = DEFAULT_FONT.copy() 989 | FONT_APP_ENTRY.config(size=10) 990 | 991 | # Style ttk 992 | style = ttk.Style() 993 | style.configure("TFrame", padding=0) 994 | style.configure("TLabel", padding=6) 995 | style.configure("TRadiobutton", padding=6) 996 | style.configure("TButton", padding=10) 997 | style.configure("TText", padding=6) 998 | 999 | ttk.Label(root, text=f"SteamAutoCracker GUI v{VERSION}", font=FONT2, padding=0).pack(pady=(10, 0), anchor="center") 1000 | ttk.Label(root, text="by BigBoiCJ", padding=0).pack(pady=(0, 0), anchor="center") 1001 | 1002 | updatesFrame = tk.Frame(root) 1003 | updatesButton = ttk.Button(updatesFrame, text="Check for updates", command=CheckUpdates, padding=0) 1004 | updatesButton.grid(row=0, column=0) 1005 | updatesFrame.pack(pady=(0, 20)) 1006 | 1007 | ttk.Button(root, text="Settings", command=SettingsButton, padding=8).pack(pady=(0, 20), anchor="center") 1008 | 1009 | """ 1010 | frame4 = ttk.Frame(root) 1011 | frame4.pack(pady=(5, 0), padx=10, anchor="center")""" 1012 | 1013 | ttk.Separator(root, orient='horizontal').pack(fill="x", padx=220) 1014 | 1015 | # Select folder fields 1016 | tk.Label(root, text="Select where your game is installed :",).pack(pady=(20, 5), anchor="center") 1017 | selectFolderButton = ttk.Button(root, text="Select a folder", command=lambda: handle_folder_selection()) 1018 | selectFolderButton.pack(pady=(0, 10)) 1019 | 1020 | selectedFolderFrame = tk.Frame(root) # This frame will contain the label. This is so we can resize the root window properly when the text is empty. 1021 | selectedFolderFrame.pack() 1022 | tk.Frame(selectedFolderFrame, width=1, height=1).pack() # 1x1 frame, else selectedFolderFrame will not update its size after it is emptied (by selectedFolderLabel.pack_forget) 1023 | selectedFolderLabel = tk.Label(selectedFolderFrame, text="", wraplength=700) 1024 | selectedFolderLabel.pack() 1025 | selectedFolderLabel.pack_forget() 1026 | 1027 | # Enter game name or appID fields 1028 | frameGame = ttk.Frame(root) # Main frame for the game 1029 | frameGame.pack(pady=(5, 0), anchor="center") 1030 | 1031 | tk.Frame(frameGame, width=1, height=1).pack() # 1x1 frame, else frameGame will not update its size after it is emptied (by frameGame2.pack_forget) 1032 | frameGame2 = ttk.Frame(frameGame) # The elements will be inside this one. This is so we can call pack_forget and still preserve the location of frameGame. 1033 | frameGame2.pack() 1034 | ttk.Separator(frameGame2, orient='horizontal').pack(fill="x", padx=50, pady=(15, 0)) 1035 | ttk.Label(frameGame2, text="Enter the Name or AppID of the game you want to Crack:").pack(pady=(15, 0), anchor="center") 1036 | 1037 | frame4 = ttk.Frame(frameGame2) 1038 | frame4.pack(pady=(5, 0), anchor="center") 1039 | gameNameEntry = tk.Entry(frame4, width=35, font=FONT_APP_ENTRY) 1040 | gameNameEntry.grid(row=0, column=0, ipady=5) 1041 | searchGameButton = ttk.Button(frame4, text="Search", padding=5, command=search_game) 1042 | searchGameButton.grid(row=0, column=1, padx=(10, 0)) 1043 | updateAppListButton = ttk.Button(frame4, text="Update the App List", padding=0, command=UpdateAppList) 1044 | 1045 | gameFoundStatus = ttk.Label(frameGame2, text="") 1046 | gameFoundStatus.pack(pady=(5, 0), anchor="center") 1047 | 1048 | frameGame2.pack_forget() # Hide the elements, but preserves their location thanks to frameGame still being packed but empty 1049 | 1050 | # Crack fields 1051 | frameCrack = ttk.Frame(root) 1052 | frameCrack.pack(pady=(15, 0), anchor="center") 1053 | tk.Frame(frameCrack, width=1, height=1).pack() # 1x1 frame 1054 | frameCrack2 = ttk.Frame(frameCrack) 1055 | frameCrack2.pack() 1056 | ttk.Separator(frameCrack2, orient='horizontal').pack(fill="x", padx=0, pady=(0, 15)) 1057 | selectedCrackFrame = ttk.Frame(frameCrack2) 1058 | selectedCrackFrame.pack() 1059 | tk.Label(selectedCrackFrame, text="Selected crack:").grid(row=0, column=0) 1060 | selectCrackButton = ttk.Button(selectedCrackFrame, text="None", padding=5, command=DisplayCrackList) 1061 | selectCrackButton.grid(row=0, column=1, padx=(10, 0)) 1062 | UpdateSelectedCrackDisplay() # Updates the text of selectCrackButton 1063 | crackGameButton = ttk.Button(frameCrack2, text="Crack the game", padding=8, command=CrackGame) 1064 | crackGameButton.pack(pady=(10, 0)) 1065 | 1066 | frameCrack2.pack_forget() # Hide the elements, but preserves their location thanks to frameCrack still being packed but empty 1067 | 1068 | # Spacer 1069 | #tk.Label(root, text="").pack() 1070 | 1071 | # Logs scroll text widget 1072 | logs_text = tk.Text(root, height=15, width=100) 1073 | logs_text.pack(pady=10, padx=10) 1074 | 1075 | text = f"SteamAutoCracker GUI v{VERSION} by BigBoiCJ" 1076 | buf = "" 1077 | for i in range(len(text)): 1078 | buf += "-" 1079 | 1080 | logs_text.insert("1.0", f"{buf}\n{text}\n{buf}") 1081 | logs_text.config(state=tk.DISABLED) # Prevents users from editing the text inside logs_text 1082 | 1083 | # Handle errors to log them while tkinter is running 1084 | root.report_callback_exception = OnTkinterError 1085 | 1086 | # Check for the existence of a leftover autoupdater 1087 | if os.path.isfile("steam_auto_cracker_gui_autoupdater.exe"): 1088 | try: 1089 | os.remove("steam_auto_cracker_gui_autoupdater.exe") 1090 | except Exception: # In case the file is locked for example 1091 | pass 1092 | 1093 | # Check for updates 1094 | if config["Preferences"]["UpdateOption"] == "1": 1095 | CheckUpdates() 1096 | 1097 | # Start main loop 1098 | root.mainloop() 1099 | 1100 | except Exception: 1101 | # Handle Python errors 1102 | print("\n[!!!] A Python error occurred! Writing the error to the error.log file.\n---") 1103 | with open("error.log", "w", encoding="utf-8") as errorFile: 1104 | errorFile.write(f"SteamAutoCracker GUI v{VERSION}\n---\nA Python error occurred!\nPlease report it on GitHub or cs.rin.ru\nMake sure to blank any personal detail.\n---\n\n") 1105 | traceback.print_exc(file=errorFile) 1106 | traceback.print_exc() 1107 | print("---\nError written to error.log, please report it on GitHub or cs.rin.ru\nMake sure to blank any personal detail.") 1108 | --------------------------------------------------------------------------------