Initial script commit
This commit is contained in:
256
main.py
Normal file
256
main.py
Normal 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.")
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
vdf
|
||||
Reference in New Issue
Block a user