├── 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 | 
3 | 
4 | 
5 | 
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 |
--------------------------------------------------------------------------------