diff --git a/steam_auto_cracker_gui.py b/steam_auto_cracker_gui.py new file mode 100644 index 0000000..d6f25c4 --- /dev/null +++ b/steam_auto_cracker_gui.py @@ -0,0 +1,937 @@ +import traceback + +try: # Handles Python errors to write them to a log file so they can be reported and fixed more easily. + import tkinter as tk + import tkinter.tix as tix + from tkinter import ttk + from tkinter import filedialog + from tkinter import font + + import requests + import configparser + import json + import os + import subprocess + from sac_lib.get_file_version import GetFileVersion + import shutil + from time import sleep + + VERSION = "2.0.0" + + RETRY_DELAY = 15 # Delay in seconds before retrying a failed request. (default, can be modified in config.ini) + 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) + + HIGH_DLC_WARNING = 125 + + folder_path = "" + appID = 0 + gameSearchDone = False + + STATE_FindingInAppList = False + STATE_UpdatingAppList = False + + EXTS_TO_REPLACE = (".txt", ".ini", ".cfg") + + def OnTkinterError(exc, val, tb): + # Handle Tkinter Python errors + print("\n[!!!] A Tkinter Python error occurred! Writing the error to the error_tkinter.log file.\n---") + with open("error_tkinter.log", "w", encoding="utf-8") as errorFile: + 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") + traceback.print_exc(file=errorFile) + traceback.print_exc() + print("---\nError written to error_tkinter.log, please report it on GitHub or cs.rin.ru\nMake sure to blank any personal detail.") + + try: + 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.") + except Exception: + pass + + class SACRequest: + def __init__(self, url:str, name:str = "Unnamed"): + self.url = url + self.tries = 0 + self.name = name + self.DoRequest() + + def DoRequest(self): + self.tries += 1 + req = requests.get(self.url, timeout=10) + if not req.ok: + if self.tries < int(config["Advanced"]["RetryMax"]): + # Do another try + update_logs("- " + self.name + " request failed, retrying in " + config["Advanced"]["RetryDelay"] + "s... (" + str(self.tries) + "/" + config["Advanced"]["RetryMax"] + " tries)") + sleep(int(config["Advanced"]["RetryDelay"])) + self.DoRequest() + else: + 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") + raise Exception(f"SACRequest: Connection failed after {config['Advanced']['RetryMax']} tries") + else: + self.req = req + + def select_folder(): + global folder_path + folder_path = filedialog.askdirectory() + if not os.path.isdir(folder_path): + update_logs("\nNo folder has been selected") + selectedFolderLabel.config(text="") + selectedFolderLabel.pack_forget() + frameGame2.pack_forget() + frameCrack2.pack_forget() # Hide the crack frame + return + + log_message = f"Sélection du dossier : {folder_path}" + update_logs(f"\nSelected folder: {folder_path}") + selectedFolderLabel.config(text=f"Selected folder:\n{folder_path}") + selectedFolderLabel.pack() + frameGame2.pack() + if gameSearchDone: + frameCrack2.pack() + + def update_logs(log_message): + # Get current content + current_logs = logs_text.get("1.0", tk.END) + + logs_text.config(state=tk.NORMAL) # Enables modification (needed to add content) + # Delete the current content + logs_text.delete("1.0", tk.END) + + # Insert the new message at the end with a linebreak + logs_text.insert(tk.END, current_logs + log_message) + + # Scroll the widget to the bottom + logs_text.yview_moveto(1.0) + + # Focus on the end + logs_text.see(tk.END) + logs_text.config(state=tk.DISABLED) # Disables modification (prevents the user from writing inside the field) + + def search_game(): + searchGameButton.config(state=tk.DISABLED) # Prevents the user from starting multiple searches at the same time + frameCrack2.pack_forget() # Hide the crack frame + global gameSearchDone + gameSearchDone = False + + gameFoundStatus.config(text=f"") + # Disable the ability to change the selected folder + selectFolderButton.config(state=tk.DISABLED) + updateAppListButton.grid_forget() + root.update() + + global appID + appID = 0 + if gameNameEntry.get() == "": + update_logs("\n[!] Please enter a valid Name or AppID") + selectFolderButton.config(state=tk.NORMAL) # Re-enable the ability to change the selected folder + return + + try: + appID = int(gameNameEntry.get()) + except: + appID = FindInAppList(gameNameEntry.get()) + + if appID != 0 and RetrieveGame(): # On success + # We are now on step 3 + gameSearchDone = True + frameCrack2.pack() # Show the crack frame + searchGameButton.config(state=tk.NORMAL) # Re-enable the ability to search the game + selectFolderButton.config(state=tk.NORMAL) # Re-enable the ability to change the selected folder + else: + searchGameButton.config(state=tk.NORMAL) # Re-enable the ability to search the game + selectFolderButton.config(state=tk.NORMAL) # Re-enable the ability to change the selected folder + + def FindInAppList(appName): + update_logs("\nImporting and searching the App List, this could take a few seconds if your computer isn't powerful enough.") + gameFoundStatus.config(text=f"Searching in the App List...") + root.update() # Update the window now + try: + with open("applist.txt", "r", encoding="utf-8") as file: + data = json.load(file) + except: + update_logs("The App List isn't downloaded on your computer, downloading it...") + UpdateAppList() + return FindInAppList(appName) # Re launch this funtion + + for elem in data["applist"]["apps"]: + if elem["name"].lower() != appName.lower(): + continue + + return elem["appid"] + + update_logs("[!] The App was not found, make sure you entered EXACTLY the Steam Game's name (watch it on Steam)") + update_logs("If you typed it properly, you can try to update the App List. Alternatively, you can try entering the AppID.") + gameFoundStatus.config(text=f"App not found!") + + updateAppListButton.grid(row=0, column=2, padx=(10, 0)) + return 0 + + def UpdateAppList(): + updateAppListButton.grid_forget() + update_logs("\nUpdating the App List, this could take a few seconds to up to a minute, depending on your internet connection.") + gameFoundStatus.config(text=f"Updating the App List...") + root.update() + try: + req = SACRequest("https://api.steampowered.com/ISteamApps/GetAppList/v2/", "UpdateAppList").req + except Exception: + gameFoundStatus.config(text=f"An error has occurred") + return + + with open("applist.txt", "w", encoding="utf-8") as file: + file.write(req.text) + update_logs("App List updated!") + gameFoundStatus.config(text=f"App List updated!") + + def RetrieveAppName(appID: int) -> str: + try: + req = SACRequest("https://store.steampowered.com/api/appdetails?appids=" + str(appID) + "&filters=basic", "RetrieveAppName").req + except Exception: + return "error" + + data = req.json() + data = data[str(appID)] + if (not "data" in data) or (not "name" in data["data"]): + return "error" + return data["data"]["name"] + + def RetrieveGame() -> bool: + global appID + global gameName + global dlcIDs + global dlcNames + + dlcIDs = [] + dlcNames = [] + + update_logs("\n[1/2] Retrieving game informations from Steam...") + gameFoundStatus.config(text=f"[1/2] Retrieving game informations from Steam...") + root.update() + # https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI#appdetails + try: + req = SACRequest("https://store.steampowered.com/api/appdetails?appids=" + str(appID) + "&filters=basic", "RetrieveGame").req + except Exception: + gameFoundStatus.config(text=f"An error has occurred") + return False + data = req.json() + data = data[str(appID)] + if not data["success"]: + update_logs(f"\n[!] AppID {appID} not found.") + gameFoundStatus.config(text=f"AppID {appID} not found.") + appID = 0 + return False + if config["Advanced"]["BypassGameVerification"] != "1" and data["data"]["type"] != "game": + update_logs(f"\n[!] AppID {appID} is not a game. You can bypass this verification in the Advanced settings.") + gameFoundStatus.config(text=f"AppID {appID} is not a game.") + appID = 0 + return False + + gameName = data["data"]["name"] + appID = data["data"]["steam_appid"] + update_logs(f"- Game found! Name: {gameName} - AppID: {appID}") + + update_logs("\n[2/2] Retrieving DLCs...") + gameFoundStatus.config(text=f"[2/2] Retrieving DLCs...") + root.update() + + # Optional config check + option = "0" + try: + option = config["Developer"]["RetrieveDLCOption"] + except: + pass + + if option == "1": + # Old retrieve option + update_logs("Using the old retrieve option (RetrieveDLCOption is set to 1)") + + if "dlc" in data["data"]: + dlcIDs = data["data"]["dlc"] + dlcIDsLen = len(dlcIDs) + + if dlcIDsLen >= HIGH_DLC_WARNING: + 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.") + + # Get DLCs names + for i in range(dlcIDsLen): + appName = RetrieveAppName(dlcIDs[i]) + if appName == "error": + update_logs(f"[!] Error! No App Name found for AppID {dlcIDs[i]}") + gameFoundStatus.config(text=f"[!] Error! No App Name found for AppID {dlcIDs[i]}") + appID = 0 + return False + dlcNames.append(appName) + update_logs("- Found DLC " + str(i+1) + "/" + str(dlcIDsLen) + ": " + appName + " (" + str(dlcIDs[i]) + ")") + gameFoundStatus.config(text=f"[2/2] Retrieving DLCs... ({i+1}/{dlcIDsLen})") + root.update() + else: + update_logs("- No DLC found for this game!") + else: + # Default retrieve option + + try: + req2 = SACRequest("https://store.steampowered.com/dlc/" + str(appID) +"/random/ajaxgetfilteredrecommendations/?query&count=10000", "RetrieveDLC").req + except Exception: + gameFoundStatus.config(text=f"An error has occurred") + return False + data2 = req2.json() + if not data2["success"]: + update_logs("[!] Retrieve DLC request failed!") + gameFoundStatus.config(text=f"Retrieve DLC request failed!") + appID = 0 + return False + + if data2["total_count"] == 0: + update_logs("- No DLC found for this game!") + else: + if data2["total_count"] >= HIGH_DLC_WARNING: + 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.") + + resultsIndex = 0 + + # format: data-ds-appid="1812883" + i = -1 + while i + 1 < data2["total_count"]: + i += 1 + + resultsStr = "" + resultsIndex = data2["results_html"].find("data-ds-appid=\"", resultsIndex) + resultsIndex += len("data-ds-appid=\"") + + while data2["results_html"][resultsIndex] != "\"": + resultsStr += data2["results_html"][resultsIndex] + resultsIndex += 1 + + dlcID = int(resultsStr) + 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. + i -= 1 + continue + dlcIDs.append(int(resultsStr)) + + # Retrieve DLC name + appName = RetrieveAppName(dlcIDs[i]) + if appName == "error": + update_logs(f"[!] Error! No App Name found for AppID {dlcIDs[i]}") + gameFoundStatus.config(text=f"Error! No App Name found for AppID {dlcIDs[i]}") + appID = 0 + return False + dlcNames.append(appName) + update_logs("- Found DLC " + str(i+1) + "/" + str(data2["total_count"]) + ": " + appName + " (" + str(dlcIDs[i]) + ")") + gameFoundStatus.config(text=f"[2/2] Retrieving DLCs... ({i+1}/{data2['total_count']})") + root.update() + + update_logs(f"Finished retrieving all the details about the game {gameName} (appID: {appID})") + gameFoundStatus.config(text=f"All details retrieved for {gameName}!") + return True # Retrieved game and DLCs successfully + + def CrackGame(): + global appID + + # Prevents the user from searching a game or selecting a folder or re-clicking the crack game button + selectFolderButton.config(state=tk.DISABLED) + searchGameButton.config(state=tk.DISABLED) + selectCrackButton.config(state=tk.DISABLED) + crackGameButton.config(state=tk.DISABLED) + + update_logs("\nSearching Steam API DLLs and cracking them...") + cracked = False + + if config["Crack"]["SelectedCrack"][:3] == "dlc" and len(dlcIDs) == 0: # If a dlc only crack has been selected, but the game has no DLC + update_logs("-----\nNo DLC is available, and you selected a DLC only crack. Aborting the cracking process.") + EndCrack() + return + + configDir = os.path.join(os.getcwd(), "sac_emu\\" + config["Crack"]["SelectedCrack"]) # "sac_emu/game_ali213" for example + try: + config.read(configDir + "\\config_override.ini") + except Exception: + pass + + configDir = os.path.join(configDir, "files") # "sac_emu/game_ali213/files" for example + + # Check if some custom Steamless options have been set up + steamlessOptions = "" + try: + steamlessOptions = config["Developer"]["SteamlessOptions"] + " " + except: + pass + + root.update() + + dllLocations = [] + for root_dir, dirs, files in os.walk(folder_path): + apiFile = "" + + # Use Steamless if configured + if config["Preferences"]["Steamless"] == "1" and crackListSteamless[config["Crack"]["SelectedCrack"]]: + # Run Steamless on every .exe file. If it's not under DRM or not the wrong file, no problem! + for fileName in files: + if not fileName.endswith(".exe"): + continue + update_logs(f"- Attempting to run Steamless on {fileName}") + root.update() + #update_logs("\n[[[ Steamless logs ]]]") + fileLocation = root_dir + "/" + fileName + shutil.move(fileLocation, fileName) # Move the file to our location + subprocess.call("Steamless_CLI\\Steamless.CLI.exe " + steamlessOptions + "\"" + fileName + "\"", shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE) # Run Steamless on the game + #update_logs("[[[ -------------- ]]]\n") + + # Check if the game was NOT unpacked + if not os.path.isfile(fileName + ".unpacked.exe"): + # Move back the original game's exe since it didn't change + update_logs("- Couldn't run Steamless on " + fileName + ", it is probably not under DRM.") + shutil.move(fileName, fileLocation) + root.update() + continue + + update_logs(f"- Removed Steam Stub DRM from {fileName}") + if config["FileNames"]["GameEXE"] != "": + # Rename and move back the original game's exe + shutil.move(fileName, fileLocation + config["FileNames"]["GameEXE"]) + else: + # Delete the original game's exe + os.remove(fileName) + # Rename and move the unpacked exe to the game's directory + shutil.move(fileName + ".unpacked.exe", fileLocation) + root.update() + + if "steam_api.dll" in files: + if config["FileNames"]["SteamAPI"] in files: + 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.") + os.remove(root_dir + "/steam_api.dll") + shutil.move(root_dir + "/" + config["FileNames"]["SteamAPI"], root_dir + "/steam_api.dll") + + apiFile = root_dir + "/steam_api.dll" + try: + apiFileVersion = GetFileVersion(apiFile) + except Exception: + update_logs("[!] steam_api.dll: could not retrieve the file version! Seems like the steam_api.dll file has already been cracked! Aborting...") + EndCrack() + return + + update_logs(f"- Found steam_api.dll in {root_dir}, planning crack application") + + if "steam_api64.dll" in files: + if config["FileNames"]["SteamAPI64"] in files: + 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.") + os.remove(root_dir + "/steam_api64.dll") + shutil.move(root_dir + "/" + config["FileNames"]["SteamAPI64"], root_dir + "/steam_api64.dll") + + apiFile = root_dir + "/steam_api64.dll" + try: + apiFileVersion = GetFileVersion(apiFile) + except Exception: + update_logs("[!] steam_api64.dll: could not retrieve the file version! Seems like the steam_api64.dll file has already been cracked! Aborting...") + EndCrack() + return + + update_logs(f"- Found steam_api64.dll in {root_dir}, planning crack application") + + if apiFile != "": + if root_dir not in dllLocations: + dllLocations.append(root_dir) + + cracked = True + root.update() + + for dllCurrentLocation in dllLocations: + for root_dir, dirs, files in os.walk(configDir): + relativeRootDir = root_dir[len(configDir) + 1:] + dllAbsoluteRelativeLocation = os.path.join(dllCurrentLocation, relativeRootDir) + + # To make it look right, add a "\" at the end of relativeRootDir if it is not empty + if len(relativeRootDir) > 0: + relativeRootDir += "\\" + + # Create all missing directories + for dir in dirs: + if not os.path.isdir(os.path.join(dllAbsoluteRelativeLocation, dir)): + os.mkdir(os.path.join(dllAbsoluteRelativeLocation, dir)) + update_logs("Created new directory " + relativeRootDir + dir) + root.update() + + # Create all files + for fileName in files: + root.update() + if os.path.isfile(os.path.join(dllAbsoluteRelativeLocation, fileName)): # The file already exists in the game, rename it to .bak + newName = fileName + config["FileNames"]["BakSuffix"] + if fileName == "steam_api.dll" or fileName == "steam_api64.dll": + if config["Preferences"]["CrackOption"] != "0": # Only create config + update_logs("Ignoring " + relativeRootDir + fileName + " because of the set crack approach") + continue + + if fileName == "steam_api.dll": + newName = config["FileNames"]["SteamAPI"] + else: + newName = config["FileNames"]["SteamAPI64"] + + if newName == "": # Don't keep a backup of the steam_api(64).dll file + os.remove(os.path.join(dllAbsoluteRelativeLocation, fileName)) + update_logs("Removed old " + relativeRootDir + fileName + " file because no backup file name is set") + elif os.path.isfile(os.path.join(dllAbsoluteRelativeLocation, newName)): # A backup of this file already exists, the game might already be cracked, abort! + 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.") + os.remove(os.path.join(dllAbsoluteRelativeLocation, fileName)) + else: + shutil.move(os.path.join(dllAbsoluteRelativeLocation, fileName), os.path.join(dllAbsoluteRelativeLocation, newName)) + update_logs("Backupped old file " + relativeRootDir + fileName + " -> " + newName) + elif fileName == "steam_api.dll" or fileName == "steam_api64.dll": # No existing file, and this file is the steam_api(64).dll one + continue # Ignore this file + + shutil.copyfile(os.path.join(root_dir, fileName), os.path.join(dllAbsoluteRelativeLocation, fileName)) + + # Check if ends with a specific extension, so we can replace the presets inside + if any(fileName.endswith(extension) for extension in EXTS_TO_REPLACE): + # Read the file's content + with open(os.path.join(dllAbsoluteRelativeLocation, fileName), "r", encoding="utf-8") as file: + fileContent = file.read() + + # Replace the presets if any + fileContent = fileContent.replace("SAC_AppID", str(appID)) + fileContent = fileContent.replace("SAC_APIVersion", apiFileVersion) + buffer = "" + for i in range(len(dlcIDs)): + buffer += str(dlcIDs[i]) + " = " + dlcNames[i] + "\n" + fileContent = fileContent.replace("SAC_DLC", buffer) + buffer = "" + for i in range(len(dlcIDs)): + buffer += str(dlcIDs[i]) + "=" + dlcNames[i] + "\n" + fileContent = fileContent.replace("SAC_NoSpaceDLC", buffer) + + # Write the changes + with open(os.path.join(dllAbsoluteRelativeLocation, fileName), "w", encoding="utf-8") as file: + file.write(fileContent) + + update_logs("Created new file " + relativeRootDir + fileName) + + + update_logs("\n-----\nFinished cracking the game!") + if not cracked: + update_logs("[!] No Steam API DLL was found in the game!") + else: + 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.)") + + EndCrack() + + def EndCrack(): + # Cracking process done! + ReloadConfig() # Reload the config to remove the overwritten config from config_override.ini + + # Now let's remove locks + selectFolderButton.config(state=tk.NORMAL) + searchGameButton.config(state=tk.NORMAL) + selectCrackButton.config(state=tk.NORMAL) + crackGameButton.config(state=tk.NORMAL) + + # ----- Settings ----- + + def SettingsButton(): + top = tk.Toplevel(root) + #top.geometry("750x250") + top.title(f"SteamAutoCracker GUI v{VERSION} - Settings") + biggerFont = DEFAULT_FONT.copy() + biggerFont.config(size=10) + ttk.Label(top, text= "Settings", font=FONT2).pack(padx=200, pady=(10,10), anchor="center") + + ttk.Button(top, text="Reset to default", padding=0, command=ResetSettingsButton).pack(pady=(0,0), anchor="center") + + # Handle scrolling + scrollCanvas = tk.Canvas(top, width=600, height=450, highlightthickness=0) + scrollCanvas.pack(pady=(5, 0), side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollFrame = ttk.Frame(scrollCanvas) + scrollCanvas.create_window((0,0), window=scrollFrame, anchor="nw") + + # Damn don't ask me how all of this works. It just does :D + scrollbar = tk.Scrollbar(top, command=scrollCanvas.yview) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + scrollCanvas.config(yscrollcommand=scrollbar.set) + + def on_mousewheel(event): + scrollCanvas.yview_scroll(int(-1*(event.delta/120)), "units") # Some magic I guess. Huge thanks to LLM who probably stole this code from someone. + + top.bind("", on_mousewheel) + + def configure_canvas(event): + scrollCanvas.config(scrollregion=scrollCanvas.bbox("all")) + + scrollFrame.bind("", configure_canvas) + # Finished handling scrolling + + # Crack approach (CrackOption) + ttk.Label(scrollFrame, text="Crack approach:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") + settings_frame1 = ttk.Frame(scrollFrame) + settings_frame1.pack(padx=(15, 0), pady=(0, 0), anchor="w") + + ## Radio + global CrackOption_var + CrackOption_var = tk.StringVar() + CrackOption_var.set(config["Preferences"]["CrackOption"]) + 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") + 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") + ttk.Radiobutton(settings_frame1, text="Only create the crack config, and put it in the same directory as the Steam Auto Cracker tool", variable=CrackOption_var, value="2", command=lambda: UpdateConfigKey("Preferences", "CrackOption", CrackOption_var.get())).grid(row=2, column=0, sticky="w") + + # Steamless (Steamless) + ttk.Label(scrollFrame, text="Steamless:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") + 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") + + settings_frame2 = ttk.Frame(scrollFrame) + settings_frame2.pack(padx=(15, 0), pady=(0, 10), anchor="w") + + ## Radio + global Steamless_var + Steamless_var = tk.StringVar() + Steamless_var.set(config["Preferences"]["Steamless"]) + 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") + 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") + + # FileNames + ttk.Label(scrollFrame, text="File names:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") + 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") + + fileNamesFrame = ttk.Frame(scrollFrame) + fileNamesFrame.pack(padx=(15, 0), pady=(0, 10), anchor="w") + + tk.Label(fileNamesFrame, text="steam_api.dll backup name:").grid(row=0, column=0) + global SteamApi_var + SteamApi_var = tk.StringVar() + steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=SteamApi_var) + steamApiEntry.grid(row=0, column=1, ipadx=10, ipady=3) + SteamApi_var.set(config["FileNames"]["SteamAPI"]) + ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("SteamAPI", SteamApi_var)).grid(row=0, column=2, ipadx=10) + + tk.Label(fileNamesFrame, text="steam_api64.dll backup name:").grid(row=1, column=0) + global SteamApi64_var + SteamApi64_var = tk.StringVar() + steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=SteamApi64_var) + steamApiEntry.grid(row=1, column=1, ipadx=10, ipady=3) + SteamApi64_var.set(config["FileNames"]["SteamAPI64"]) + ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("SteamAPI64", SteamApi64_var)).grid(row=1, column=2, ipadx=10) + + tk.Label(fileNamesFrame, text="Game EXE backup suffix:").grid(row=2, column=0) + global GameEXE_var + GameEXE_var = tk.StringVar() + steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=GameEXE_var) + steamApiEntry.grid(row=2, column=1, ipadx=10, ipady=3) + GameEXE_var.set(config["FileNames"]["GameEXE"]) + ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("GameEXE", GameEXE_var)).grid(row=2, column=2, ipadx=10) + + tk.Label(fileNamesFrame, text="Other files backup suffix:").grid(row=3, column=0) + global BakSuffix_var + BakSuffix_var = tk.StringVar() + steamApiEntry = tk.Entry(fileNamesFrame, width=35, textvariable=BakSuffix_var) + steamApiEntry.grid(row=3, column=1, ipadx=10, ipady=3) + BakSuffix_var.set(config["FileNames"]["BakSuffix"]) + ttk.Button(fileNamesFrame, text="Save", padding=3, command=lambda: UpdateFileName("BakSuffix", BakSuffix_var)).grid(row=3, column=2, ipadx=10) + + # Advanced + ttk.Label(scrollFrame, text="Advanced:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") + 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") + + advTextFrame = ttk.Frame(scrollFrame) + advTextFrame.pack(padx=(15, 0), pady=(0, 10), anchor="w") + + tk.Label(advTextFrame, text="RetryDelay:").grid(row=0, column=0) + global RetryDelay_var + RetryDelay_var = tk.StringVar() + steamApiEntry = tk.Entry(advTextFrame, width=35, textvariable=RetryDelay_var) + steamApiEntry.grid(row=0, column=1, ipadx=10, ipady=3) + RetryDelay_var.set(config["Advanced"]["RetryDelay"]) + ttk.Button(advTextFrame, text="Save", padding=3, command=lambda: UpdateAdvanced("RetryDelay", RetryDelay_var)).grid(row=0, column=2, ipadx=10) + + tk.Label(advTextFrame, text="RetryMax:").grid(row=1, column=0) + global RetryMax_var + RetryMax_var = tk.StringVar() + steamApiEntry = tk.Entry(advTextFrame, width=35, textvariable=RetryMax_var) + steamApiEntry.grid(row=1, column=1, ipadx=10, ipady=3) + RetryMax_var.set(config["Advanced"]["RetryMax"]) + ttk.Button(advTextFrame, text="Save", padding=3, command=lambda: UpdateAdvanced("RetryMax", RetryMax_var)).grid(row=1, column=2, ipadx=10) + + global BypassGameVerification_var + BypassGameVerification_var = tk.StringVar() + BypassGameVerification_var.set(config["Advanced"]["BypassGameVerification"]) + 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)) + advBypassGameVerification.pack(padx=(15, 0), pady=(0, 10), anchor="w") + + top.grab_set() # Catches all interactions, prevents the user from interacting with the root window + + def UpdateFileName(key, strVar): + value = strVar.get().strip() + strVar.set(value) + UpdateConfigKey("FileNames", key, value) + + def UpdateAdvanced(key, strVar): + value = strVar.get().strip() + try: + int(value) + except: + strVar.set(config["Advanced"][key]) + else: # If no error + strVar.set(value) + UpdateConfigKey("Advanced", key, value) + + def ResetSettingsButton(): + ResetConfig(1) + + # Update the radio buttons values + CrackOption_var.set(config["Preferences"]["CrackOption"]) + Steamless_var.set(config["Preferences"]["Steamless"]) + SteamApi_var.set(config["FileNames"]["SteamAPI"]) + SteamApi64_var.set(config["FileNames"]["SteamAPI64"]) + GameEXE_var.set(config["FileNames"]["GameEXE"]) + BakSuffix_var.set(config["FileNames"]["BakSuffix"]) + RetryDelay_var.set(config["Advanced"]["RetryDelay"]) + RetryMax_var.set(config["Advanced"]["RetryMax"]) + BypassGameVerification_var.set(config["Advanced"]["BypassGameVerification"]) + + # ----- Crack List ----- + + crackList = { # A list of all selectable cracks + "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."], + "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."], + "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."] + } + + crackListSteamless = { # Whether to use Steamless with a specific crack. True = use Steamless + "game_ali213": True, + "game_goldberg": True, + "dlc_creamapi": False + } + + def DisplayCrackList(): + top = tk.Toplevel(root) + top.title(f"SteamAutoCracker GUI v{VERSION} - Crack List") + biggerFont = DEFAULT_FONT.copy() + biggerFont.config(size=10) + ttk.Label(top, text= "Crack List", font=FONT2).pack(padx=200, pady=(10,10), anchor="center") + + ttk.Button(top, text="Reset to default", padding=0, command=ResetCrackListButton).pack(pady=(0,0), anchor="center") + + # Selected crack (SelectedCrack) + ttk.Label(top, text="Selected crack:", font=FONT3, padding=0).pack(padx=(6, 0), pady=(10,0), anchor="w") + settings_frame1 = ttk.Frame(top) + settings_frame1.pack(padx=(15, 0), pady=(0, 0), anchor="w") + + ## Radio + global SelectedCrack_var + SelectedCrack_var = tk.StringVar() + SelectedCrack_var.set(config["Crack"]["SelectedCrack"]) + rowNum = 0 + for k, v in crackList.items(): + ttk.Radiobutton(settings_frame1, text=v[0], variable=SelectedCrack_var, value=k, command=lambda: UpdateSelectedCrack()).grid(row=rowNum, column=0, sticky="w") + rowNum += 1 + if len(v) > 1: # Contains a description + tk.Label(settings_frame1, text=v[1], font=FONT4, foreground="#575757", wraplength=700, justify="left").grid(row=rowNum, column=0, sticky="w", ipadx=20) + rowNum += 1 + + # Spacer + tk.Label(top, text="").pack() + + top.grab_set() # Catches all interactions, prevents the user from interacting with the root window + + def UpdateSelectedCrack(): + value = SelectedCrack_var.get() + UpdateConfigKey("Crack", "SelectedCrack", value) + UpdateSelectedCrackDisplay() + + def UpdateSelectedCrackDisplay(): + selectCrackButton.config(text=crackList[config["Crack"]["SelectedCrack"]][0]) # Display the name of the selected crack on the select crack button in the root window + + def ResetCrackListButton(): + ResetConfig(2) + + # Update the radio buttons values + SelectedCrack_var.set(config["Crack"]["SelectedCrack"]) + + # Update the root button's text + UpdateSelectedCrackDisplay() + + # --------------------------------------- + + def UpdateConfig(): + with open("config.ini", "w", encoding="utf-8") as configFile: + config.write(configFile) + + def UpdateConfigKey(section: str, key: str, value: str): + config[section][key] = value + UpdateConfig() + + def ResetConfig(resetLevel = 0, customConfig=None): + """resetLevel values: + 0 = Everything + 1 = Main settings only (Preferences, FileNames, Advanced) + 2 = Crack selection settings only (Crack) + """ + if customConfig: + currentConfig = customConfig + else: + currentConfig = config + + if resetLevel == 0 or resetLevel == 1: + currentConfig["Preferences"] = {} + currentConfig["Preferences"]["CrackOption"] = "0" + currentConfig["Preferences"]["Steamless"] = "1" + + currentConfig["FileNames"] = {} + currentConfig["FileNames"]["GameEXE"] = ".bak" + currentConfig["FileNames"]["BakSuffix"] = ".bak" + currentConfig["FileNames"]["SteamAPI"] = "steam_api.dll.bak" + currentConfig["FileNames"]["SteamAPI64"] = "steam_api64.dll.bak" + + currentConfig["Advanced"] = {} + currentConfig["Advanced"]["RetryDelay"] = str(RETRY_DELAY) + currentConfig["Advanced"]["RetryMax"] = str(RETRY_MAX) + currentConfig["Advanced"]["BypassGameVerification"] = "0" + if resetLevel == 0 or resetLevel == 2: + currentConfig["Crack"] = {} + currentConfig["Crack"]["SelectedCrack"] = "game_ali213" + + if not customConfig: + UpdateConfig() + + def FillConfig(currentConfig, configDefault): + changed = False + for k, v in configDefault.items(): + if k not in currentConfig: + currentConfig[k] = v + print("Updated", k, "->", v) + changed = True + if type(v) == configparser.SectionProxy: + if FillConfig(currentConfig[k], v): + changed = True + + return changed + + def ReloadConfig(): + global config + config = configparser.ConfigParser() + + if config.read("config.ini") == []: + # Config doesn't exist, create it + ResetConfig() + else: + # Create a config with default values + configDefault = configparser.ConfigParser() + ResetConfig(0, configDefault) + + # Check if the config is complete. If not, complete it. + changed = FillConfig(config, configDefault) + if changed: + print("[SAC] config.ini has been updated, missing entries have been created") + UpdateConfig() + + ReloadConfig() + + # --------------------------------------- + + + # Let's now create the main window + root = tk.Tk() + root.title(f"SteamAutoCracker GUI v{VERSION}") + + DEFAULT_FONT = font.nametofont('TkTextFont') + FONT2 = DEFAULT_FONT.copy() + FONT2.config(size=15) + FONT3 = DEFAULT_FONT.copy() + FONT3.config(size=12) + FONT4 = DEFAULT_FONT.copy() + FONT4.config(size=8) + FONT_APP_ENTRY = DEFAULT_FONT.copy() + FONT_APP_ENTRY.config(size=10) + + # Style ttk + style = ttk.Style() + style.configure("TFrame", padding=0) + style.configure("TLabel", padding=6) + style.configure("TRadiobutton", padding=6) + style.configure("TButton", padding=10) + style.configure("TText", padding=6) + + ttk.Label(root, text=f"SteamAutoCracker GUI v{VERSION}", font=FONT2, padding=0).pack(pady=(10, 0), anchor="center") + ttk.Label(root, text="by BigBoiCJ", padding=0).pack(pady=(0, 20), anchor="center") + + ttk.Button(root, text="Settings", command=SettingsButton, padding=8).pack(pady=(0, 20), anchor="center") + + """ + frame4 = ttk.Frame(root) + frame4.pack(pady=(5, 0), padx=10, anchor="center")""" + + ttk.Separator(root, orient='horizontal').pack(fill="x", padx=220) + + # Select folder fields + tk.Label(root, text="Select where your game is installed :",).pack(pady=(20, 5), anchor="center") + selectFolderButton = ttk.Button(root, text="Select a folder", command=select_folder) + selectFolderButton.pack(pady=(0, 10)) + + 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. + selectedFolderFrame.pack() + 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) + selectedFolderLabel = tk.Label(selectedFolderFrame, text="", wraplength=700) + selectedFolderLabel.pack() + selectedFolderLabel.pack_forget() + + # Enter game name or appID fields + frameGame = ttk.Frame(root) # Main frame for the game + frameGame.pack(pady=(5, 0), anchor="center") + + 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) + 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. + frameGame2.pack() + ttk.Separator(frameGame2, orient='horizontal').pack(fill="x", padx=50, pady=(15, 0)) + ttk.Label(frameGame2, text="Enter the Name or AppID of the game you want to Crack:").pack(pady=(15, 0), anchor="center") + + frame4 = ttk.Frame(frameGame2) + frame4.pack(pady=(5, 0), anchor="center") + gameNameEntry = tk.Entry(frame4, width=35, font=FONT_APP_ENTRY) + gameNameEntry.grid(row=0, column=0, ipady=5) + searchGameButton = ttk.Button(frame4, text="Search", padding=5, command=search_game) + searchGameButton.grid(row=0, column=1, padx=(10, 0)) + updateAppListButton = ttk.Button(frame4, text="Update the App List", padding=0, command=UpdateAppList) + + gameFoundStatus = ttk.Label(frameGame2, text="") + gameFoundStatus.pack(pady=(5, 0), anchor="center") + + frameGame2.pack_forget() # Hide the elements, but preserves their location thanks to frameGame still being packed but empty + + # Crack fields + frameCrack = ttk.Frame(root) + frameCrack.pack(pady=(15, 0), anchor="center") + tk.Frame(frameCrack, width=1, height=1).pack() # 1x1 frame + frameCrack2 = ttk.Frame(frameCrack) + frameCrack2.pack() + ttk.Separator(frameCrack2, orient='horizontal').pack(fill="x", padx=0, pady=(0, 15)) + selectedCrackFrame = ttk.Frame(frameCrack2) + selectedCrackFrame.pack() + tk.Label(selectedCrackFrame, text="Selected crack:").grid(row=0, column=0) + selectCrackButton = ttk.Button(selectedCrackFrame, text="None", padding=5, command=DisplayCrackList) + selectCrackButton.grid(row=0, column=1, padx=(10, 0)) + UpdateSelectedCrackDisplay() # Updates the text of selectCrackButton + crackGameButton = ttk.Button(frameCrack2, text="Crack the game", padding=8, command=CrackGame) + crackGameButton.pack(pady=(10, 0)) + + frameCrack2.pack_forget() # Hide the elements, but preserves their location thanks to frameCrack still being packed but empty + + # Spacer + #tk.Label(root, text="").pack() + + # Logs scroll text widget + logs_text = tk.Text(root, height=15, width=100) + logs_text.pack(pady=10, padx=10) + + text = f"SteamAutoCracker GUI v{VERSION} by BigBoiCJ" + buf = "" + for i in range(len(text)): + buf += "-" + + logs_text.insert("1.0", f"{buf}\n{text}\n{buf}") + logs_text.config(state=tk.DISABLED) # Prevents users from editing the text inside logs_text + + # Handle errors to log them while tkinter is running + root.report_callback_exception = OnTkinterError + # Start main loop + root.mainloop() + +except Exception: + # Handle Python errors + print("\n[!!!] A Python error occurred! Writing the error to the error.log file.\n---") + with open("error.log", "w", encoding="utf-8") as errorFile: + 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") + traceback.print_exc(file=errorFile) + traceback.print_exc() + print("---\nError written to error.log, please report it on GitHub or cs.rin.ru\nMake sure to blank any personal detail.") \ No newline at end of file