Files
csgo-skinchanger/main.py
2023-05-26 15:26:08 +02:00

257 lines
8.8 KiB
Python

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