diff --git a/Tools/genshin_merge_mods.py b/Tools/genshin_merge_mods.py index 4c87374..1054c0b 100644 --- a/Tools/genshin_merge_mods.py +++ b/Tools/genshin_merge_mods.py @@ -20,6 +20,9 @@ import re import argparse import hashlib +import sys +import traceback + def main(): parser = argparse.ArgumentParser(description="Generates a merged mod from several mod folders") @@ -31,6 +34,7 @@ def main(): parser.add_argument("-c", "--compress", action="store_true", help="Makes the output mods as small as possible (warning: difficult to reverse, have backups)") parser.add_argument("-a", "--active", action="store_true", default=True, help="Only active character gets swapped when swapping)") parser.add_argument("-ref", "--reflection", action="store_true", help="Applies reflection fix for 3.0+ characters") + parser.add_argument("-w", "--weapon", action="store_true", help="Use to merge weapons") args = parser.parse_args() @@ -72,11 +76,31 @@ def main(): if args.key: key = args.key else: - print("\nPlease enter the key that will be used to cycle mods (can also enter this with -k flag, or set later in .ini). Key must be a single letter\n") - key = input() - while not key or len(key) != 1: - print("\nKey not recognized, must be a single letter\n") + print("\nPlease enter the key that will be used to cycle mods (can also enter this with -k flag, or set later in .ini)") + print("Key must be a single letter. You can also assign a second key after a slash (key1 / key2) to cycle backwards\nPress ENTER to use the default keys (default is right/left keys)") + while True: key = input() + if key == '' or key == ' ': + key = 'vk_right\nback = vk_left' + break + else: + if len(key) == 1: + break + key = key.split('/', 1) + if len(key) == 2: + if len(key[0]) != 1: + if key[0].endswith(' '): + key[0] = key[0][:-1] + if len(key[1]) != 1: + if key[1].startswith(' '): + key[1] = key[1][1:] + if len(key[0]) == 1 and len(key[1]) == 1: + if not key[0] == ' ' and not key[1] == ' ': + if not key[0] == key[1]: + key = f'{key[0]}\nback = {key[1]}' + break + print("\nKey not recognized, must be a single letter\n") + key = key.lower() constants = "; Constants ---------------------------\n\n" @@ -84,26 +108,8 @@ def main(): shader = "; Shader ------------------------------\n\n" commands = "; CommandList -------------------------\n\n" resources = "; Resources ---------------------------\n\n" - swapvar = "swapvar" - constants += f"[Constants]\nglobal persist ${swapvar} = 0\n" - if args.active: - constants += f"global $active\n" - if args.reflection: - constants += f"global $reflection\n" - constants += "global $creditinfo = 0\n" - constants += f"\n[KeySwap]\n" - if args.active: - constants += f"condition = $active == 1\n" - constants += f"key = {key}\ntype = cycle\n${swapvar} = {','.join([str(x) for x in range(len(ini_files))])}\n$creditinfo = 0\n\n" - if args.active or args.reflection: - constants += f"[Present]\n" - if args.active: - constants += f"post $active = 0\n" - if args.reflection: - constants += f"post $reflection = 0\n" - - + print("Parsing ini sections") all_mod_data = [] @@ -118,10 +124,6 @@ def main(): all_mod_data.append(mod_data) ini_group += 1 - if [x for x in all_mod_data if "name" in x and x["name"].lower() == "creditinfo"]: - constants += "run = CommandListCreditInfo\n\n" - else: - constants += "\n" if [x for x in all_mod_data if "name" in x and x["name"].lower() == "transparency"]: shader += """[CustomShaderTransparency] @@ -140,7 +142,28 @@ def main(): seen_hashes = {} reflection = {} n = 1 + skip = None + dont_skip = None + merged_weapon = False + dont_disable = [] + swapvars = [] for i in range(len(all_mod_data)): + if skip == int(all_mod_data[i]['ini_group']): + continue + else: + skip = None + + skip_inis = ['glider', 'icon'] # TODO: Option to merge them with a choosen mod? + if not dont_skip == all_mod_data[i]['ini_group'] and any(x in f'{os.path.basename(os.path.dirname(ini_files[all_mod_data[i]["ini_group"]]))}\\{os.path.basename(ini_files[all_mod_data[i]["ini_group"]])}'.lower() for x in skip_inis): + print(f'\nFound a possible glider/icon .ini ({os.path.basename(os.path.dirname(ini_files[all_mod_data[i]["ini_group"]]))}\\{os.path.basename(ini_files[all_mod_data[i]["ini_group"]])}), would you like to skip it?') + choice = input('(Y/N): ') + if choice.lower() == 'y': + skip = int(all_mod_data[i]['ini_group']) + dont_disable.append(ini_files[all_mod_data[i]['ini_group']]) + continue + else: + dont_skip = all_mod_data[i]['ini_group'] + # Overrides. Since we need these to generate the command lists later, need to store the data if "hash" in all_mod_data[i]: index = -1 @@ -162,6 +185,10 @@ def main(): if args.active: if "Position" in all_mod_data[i]["name"]: overrides += f"$active = 1\n" + if args.weapon: + if "TextureOverride" in all_mod_data[i]["header"] and not merged_weapon: + overrides += "$active = 1\n" + merged_weapon = True overrides += "\n" # Otherwise, we have seen the hash before and we just need to append it to the commandlist @@ -171,12 +198,21 @@ def main(): command_data.setdefault((all_mod_data[i]["name"],0),[]).append(all_mod_data[i]) # Resources elif "filename" in all_mod_data[i] or "type" in all_mod_data[i]: - resources += f"[{all_mod_data[i]['header']}{all_mod_data[i]['name']}.{all_mod_data[i]['ini_group']}]\n" for command in all_mod_data[i]: if command in ["header", "name", "location", "ini_group"]: continue if command == "filename": + if not os.path.isfile(f"{all_mod_data[i]['location']}\\{all_mod_data[i][command]}"): + print(f'\nWARNING: Missing file: "{all_mod_data[i]["location"]}\\{all_mod_data[i][command]}".\nWould you like to force it anyways and continue or skip this .ini ({ini_files[all_mod_data[i]["ini_group"]]})?') + choice = input('(Y/N | Skip): ') + if choice.lower() == "y": + continue + elif choice.lower() == "skip": + skip = all_mod_data[i]['ini_group'] + break + else: + raise(KeyboardInterrupt) with open(f"{all_mod_data[i]['location']}\\{all_mod_data[i][command]}", "rb") as f: sha1 = hashlib.sha1(f.read()).hexdigest() if sha1 in seen_hashes and args.compress: @@ -187,6 +223,10 @@ def main(): resources += f"{command} = {all_mod_data[i]['location']}\\{all_mod_data[i][command]}\n" else: resources += f"{command} = {all_mod_data[i][command]}\n" + + if not all_mod_data[i]['ini_group'] in swapvars and not skip: + swapvars.append(all_mod_data[i]['ini_group']) + resources += "\n" if args.reflection: @@ -267,7 +307,29 @@ def main(): commands = commands.rsplit("else if",1)[0] + "endif\n\n" print("Printing results") - result = f"; Merged Mod: {', '.join([x for x in ini_files])}\n\n" + constants += f"[Constants]\nglobal persist ${swapvar} = 0\n" + if args.active: + constants += f"global $active\n" + if args.reflection: + constants += f"global $reflection\n" + constants += "global $creditinfo = 0\n" + constants += f"\n[KeySwap]\n" + if args.active: + constants += f"condition = $active == 1\n" + constants += f"key = {key}\ntype = cycle\n${swapvar} = {','.join([str(x) for x in swapvars])}\n$creditinfo = 0\n\n" + if args.active or args.reflection: + constants += f"[Present]\n" + if args.active: + constants += f"post $active = 0\n" + if args.reflection: + constants += f"post $reflection = 0\n" + + if [x for x in all_mod_data if "name" in x and x["name"].lower() == "creditinfo"]: + constants += "run = CommandListCreditInfo\n\n" + else: + constants += "\n" + + result = f"; Merged Mod: {', '.join([ini_files[x] for x in swapvars])}\n\n" if args.reflection: result += f"{refresources}\n" result += constants @@ -287,29 +349,50 @@ def main(): f.write(result) if not args.store: - print("Cleanup and disabling ini") + print("Cleanup and disabling ini") # FIX: it doesn't disable inis that wasn't included on get_user_order? for file in ini_files: - os.rename(file, os.path.join(os.path.dirname(file), "DISABLED") + os.path.basename(file)) + if file in dont_disable: + continue + ini_name = os.path.join(os.path.dirname(file), "DISABLED") + os.path.basename(file) + if os.path.isfile(ini_name): + for i in range(sys.maxsize): + if not os.path.isfile(f'{os.path.dirname(file)}\\DISABLED_Backup_{f"{os.path.basename(file)}" if i == 0 else f"{os.path.splitext(os.path.basename(file))[0]} ({i}){os.path.splitext(ini_name)[1]}"}'): + os.rename(ini_name, f'{os.path.dirname(file)}\\DISABLED_Backup_{f"{os.path.basename(file)}" if i == 0 else f"{os.path.splitext(os.path.basename(file))[0]} ({i}){os.path.splitext(ini_name)[1]}"}') + break + os.rename(file, ini_name) print("All operations completed") +def atoi(text): + return int(text) if text.isdigit() else text + +def natural_keys(text): + ''' + alist.sort(key=natural_keys) sorts in human order + http://nedbatchelder.com/blog/200712/human_sorting.html + https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside + ''' + return [ atoi(c) for c in re.split(r'(\d+)', text) ] + # Collects all .ini files from current folder and subfolders def collect_ini(path, ignore): ini_files = [] for root, dir, files in os.walk(path): if "disabled" in root.lower(): continue + dir.sort(key=natural_keys) for file in files: if "disabled" in file.lower() or ignore.lower() in file.lower(): continue if os.path.splitext(file)[1] == ".ini": - ini_files.append(os.path.join(root, file)) + if not file == 'desktop.ini': + ini_files.append(os.path.join(root, file)) return ini_files # Re-enables disabled ini files -def enable_ini(path): +def enable_ini(path): # FIX: Error if the subfolder of a .ini is disabled for root, dir, files in os.walk(path): for file in files: if os.path.splitext(file)[1] == ".ini" and ("disabled" in root.lower() or "disabled" in file.lower()): @@ -391,6 +474,13 @@ def parse_section(section): return mod_data +def handle_traceback(exc_type, exc_value, exc_tb): + traceback.print_exception(exc_type, exc_value, exc_tb) + os.system('pause') + sys.exit(-1) + +sys.excepthook = handle_traceback if __name__ == "__main__": main() + os.system('pause') \ No newline at end of file