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.")