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