From 118493345f54ce07bd1ad1d24a026f0942eb4d25 Mon Sep 17 00:00:00 2001 From: paumann Date: Fri, 26 May 2023 15:25:11 +0200 Subject: [PATCH] Initial script commit --- main.py | 256 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 257 insertions(+) create mode 100644 main.py create mode 100644 requirements.txt diff --git a/main.py b/main.py new file mode 100644 index 0000000..37cf99d --- /dev/null +++ b/main.py @@ -0,0 +1,256 @@ +import argparse +import logging +import random +import shutil +from difflib import SequenceMatcher +from operator import itemgetter +from pathlib import Path + +import vdf +from win32api import GetLogicalDriveStrings + +CSGO_STEAM_RELPATH = "steamapps/common/Counter-Strike Global Offensive/csgo" +POSSIBLE_CSGO_LOCATIONS = [ + "Program Files (x86)/Steam", + "Program Files/Steam", + "SteamLibrary", +] +BACKUP_MAGIC_STRING = "skinchanger" + + +def find_csgo() -> Path: + """ + Returns the csgo steam directory, if possible. + """ + drives = [Path(d) for d in GetLogicalDriveStrings().split("\000")] + + # Try all drives and all possible locations + for drive in drives: + for location in POSSIBLE_CSGO_LOCATIONS: + suspect = drive / location / CSGO_STEAM_RELPATH + if suspect.exists(): + return suspect + + return None + + +def is_insecure(localconfig: Path) -> bool: + """ + Returns True if the '-insecure' launch flag is set. + """ + with open(localconfig, "r", encoding="utf8") as localconfig_file: + config = vdf.load(localconfig_file) + csgo_config = config["UserLocalConfigStore"]["Software"]["valve"]["steam"]["apps"]["730"] + return "-insecure" in csgo_config["LaunchOptions"] + + +def similar_names(name: str, candidates: list[str], n: int = 1) -> list[str]: + """ + Returns n candidates similar to the name parameter. + """ + + # Calculate similarities + similarities = [SequenceMatcher(None, name, c).ratio() for c in candidates] + candidates = zip(candidates, similarities) + + # Remove candidates with similarity = 0 + candidates = [(candidate, similarity) for candidate, similarity in candidates if similarity > 0] + candidates = [candidate for candidate, _ in sorted(candidates, key=itemgetter(1), reverse=True)] + + return candidates[: min(len(candidates), n)] + + +def selection(options: list[str], msg: str) -> str: + """ + Prints the message aswell as the supplied options, + and prompts the user to pick one of the options. + """ + print(msg) + for index, option in enumerate(options, start=1): + print(f" [{index}] {option}") + while True: + selection = input(f"Please select a number between {1} and {len(options)}: ") + try: + selection = int(selection) - 1 + except ValueError: + continue + if selection >= 0 and selection < len(options): + return options[selection] + + +def select_skin(skins: dict, msg: str) -> str: + """ + Prompts the user to enter a display skin name, and returns a valid + skin name (as used in items_game.txt) + """ + request = input(msg).strip().lower() + try: + requested_skins = skins[request.strip()] + if len(requested_skins) == 1: + return requested_skins[0] + else: + return selection(requested_skins, "Please select one of these matching skins:") + except KeyError: + similar = similar_names(request, skins.keys(), n=3) + print(f"Not found! Similar skins: {[similar]}") + return None + + +def replace_skin(dest: str, src: str, items_game_dict: vdf.VDFDict) -> bool: + """ + Replaces skin dest with skin src. + Returns true if replacement was successful. + """ + paint_kits = items_game_dict["items_game"].get_all_for("paint_kits") + + dest_id = None + dest_paintkit_dict = None + src_paintkit = None + + for paintkit_dict in paint_kits: + for key, value in paintkit_dict.items(): + if value["name"] == dest: + dest_id = key + dest_paintkit_dict = paintkit_dict + elif value["name"] == src: + src_paintkit = value + + # If any variable was not found + if dest_id is None or dest_paintkit_dict is None or src_paintkit is None: + return False + + # Copy all values except for the name + dest_paintkit = vdf.VDFDict({"name": dest}) + for key, value in src_paintkit.items(): + dest_paintkit[key] = value + dest_paintkit_dict[(0, dest_id)] = dest_paintkit + + return True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--csgo", + "-c", + type=Path, + help="CSGO directory. Assumes default windows path if not specified.", + ) + parser.add_argument( + "--localconfig", + "-l", + type=Path, + help="Steam user localconfig.vdf file location. If specified, will be used to check if the '-insecure' launch option is set.", + ) + parser.add_argument( + "--language", + type=str, + default="english", + help="Language, in which skin names will be entered. Defaults to english.", + ) + args = parser.parse_args() + + # Logging + logging.basicConfig(level=logging.INFO) + + # Find csgo directory if required + csgo = args.csgo if args.csgo is not None else find_csgo() + if csgo is None: + logging.error("Could not find csgo directory. Please specify using the '--csgo' parameter.") + exit() + else: + logging.info(f"Using csgo directory found under '{csgo}'") + + # Make sure '-insecure' launch option is enabled + if args.localconfig is not None and not is_insecure(args.localconfig): + logging.error(f"Launch option '-insecure' check failed.") + exit() + + # Find 'items_game' file + items_game = csgo / "scripts/items/items_game.txt" + if not items_game.exists(): + logging.error(f"Could not find 'items_game.txt' file (Should be at {items_game}).") + exit() + + # Create backup (if it doesn't already exist) + items_game_backup = items_game.with_name(f"items_game_backup_{BACKUP_MAGIC_STRING}.txt") + if not items_game_backup.exists(): + shutil.copyfile(items_game, items_game_backup) + logging.info("Backed up 'items_game.txt' file.") + else: + logging.info("Backup already exists.") + + # Load 'items_game' file + logging.info("Loading 'items_game.txt' ...") + with open(items_game, "r", encoding="utf8") as items_game_file: + items_game_dict = vdf.load(items_game_file, mapper=vdf.VDFDict) + paint_kits = items_game_dict["items_game"].get_all_for("paint_kits") + logging.info(f"Done! Found {sum([len(pk) for pk in paint_kits])} skins!") + + # Load 'csgo_english.txt' resource for skin names + logging.info(f"Loading 'csgo_{args.language}.txt' ...") + names = csgo / "resource" / f"csgo_{args.language}.txt" + if not names.exists(): + logging.error(f"Could not find request language resource file. Make sure the language is set correctly.") + with open(names, "r", encoding="utf16") as names_file: + names_dict = vdf.load(names_file) + + # Only use skin names + tokens = names_dict["lang"]["Tokens"] + skins = dict() + for key, value in tokens.items(): + if not (key.startswith("PaintKit") and key.endswith("Tag")): + continue + skin = skins.setdefault(value.lower(), []) + skin.append(key.lstrip("PaintKit_").rstrip("_Tag")) + logging.info(f"Done! Found {len(skins)} unique skin names!") + + # For each skin name, print the id + while True: + choice = selection(("Replace skin", "Randomize", "Save & Exit", "Restore"), "What do you want to do?") + + # Save the items_game.txt file and exit + if choice == "Save & Exit": + logging.info("Saving ...") + with open(items_game, "w") as items_game_file: + vdf.dump(items_game_dict, items_game_file, pretty=True) + logging.info("Done! Bye") + exit() + + # Restores the items_game.txt file with the backup + elif choice == "Restore": + shutil.copyfile(items_game_backup, items_game) + logging.info("Successfully restored.") + + # Replace a single skin + elif choice == "Replace skin": + try: + dest = None + while dest is None: + dest = select_skin(skins, "Please enter the name of the skin to be changed: ") + + src = None + while src is None: + src = select_skin(skins, "Please enter with which skin this skin should be replaced: ") + except KeyboardInterrupt: + break + + if replace_skin(dest, src, items_game_dict): + logging.info(f"Replaced '{dest}' with '{src}'!") + else: + logging.error("Could not replace skins: Unknown error") + + # Randomize all skins + elif choice == "Randomize": + identifiers = [identifier for skin_group in skins.values() for identifier in skin_group] + + mapping = dict(zip(identifiers, random.sample(identifiers, len(identifiers)))) + for index, (dest, src) in enumerate(mapping.items(), start=1): + print(f"Shuffling ... ({index}/{len(mapping)})", end="\r") + replace_skin(dest, src, items_game_dict) + + print("\n") + logging.info("All skins shuffled!") + + else: + logging.error("Unknown option selected.") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6fbefc6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +vdf \ No newline at end of file