Initial script commit

This commit is contained in:
2023-05-26 15:25:11 +02:00
parent 0173877709
commit 118493345f
2 changed files with 257 additions and 0 deletions

256
main.py Normal file
View File

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