From 73aa224a8d0d5e34e2d2f53f3553f8d3dd04f34c Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 8 Dec 2023 04:39:08 -0300 Subject: [PATCH 1/4] Fix missing/already existing files error --- Tools/genshin_merge_mods.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Tools/genshin_merge_mods.py b/Tools/genshin_merge_mods.py index 4c87374..883b7e1 100644 --- a/Tools/genshin_merge_mods.py +++ b/Tools/genshin_merge_mods.py @@ -20,6 +20,11 @@ import re import argparse import hashlib +import sys + +import logging #debugging +logging.basicConfig(level=logging.DEBUG) + def main(): parser = argparse.ArgumentParser(description="Generates a merged mod from several mod folders") @@ -140,7 +145,12 @@ def main(): seen_hashes = {} reflection = {} n = 1 + skip = None for i in range(len(all_mod_data)): + if skip == int(all_mod_data[i]['ini_group']): + continue + else: + skip = None # 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 @@ -177,6 +187,16 @@ def main(): 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"]]})?') + response = input('(Y/N | Skip): ') + if response.lower() == "y": + continue + elif response.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: @@ -289,7 +309,13 @@ def main(): if not args.store: print("Cleanup and disabling ini") for file in ini_files: - os.rename(file, os.path.join(os.path.dirname(file), "DISABLED") + os.path.basename(file)) + 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") From e2be7d58773c50c4116134a5eb553040fc9a79a2 Mon Sep 17 00:00:00 2001 From: Artprozew Date: Sat, 9 Dec 2023 03:50:46 -0300 Subject: [PATCH 2/4] Fix ignore desktop.ini Feat human sorting order --- Tools/genshin_merge_mods.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Tools/genshin_merge_mods.py b/Tools/genshin_merge_mods.py index 883b7e1..8f00731 100644 --- a/Tools/genshin_merge_mods.py +++ b/Tools/genshin_merge_mods.py @@ -22,9 +22,6 @@ import hashlib import sys -import logging #debugging -logging.basicConfig(level=logging.DEBUG) - def main(): parser = argparse.ArgumentParser(description="Generates a merged mod from several mod folders") @@ -321,17 +318,30 @@ def main(): 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 From da3730d50722e35cce33bbb48dc91e2ac83532e6 Mon Sep 17 00:00:00 2001 From: Artprozew Date: Sat, 9 Dec 2023 15:25:58 -0300 Subject: [PATCH 3/4] Feat: add cycle backwards key Feat: handle exits and exceptions --- Tools/genshin_merge_mods.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/Tools/genshin_merge_mods.py b/Tools/genshin_merge_mods.py index 8f00731..97c48b7 100644 --- a/Tools/genshin_merge_mods.py +++ b/Tools/genshin_merge_mods.py @@ -21,7 +21,7 @@ import argparse import hashlib import sys - +import traceback def main(): parser = argparse.ArgumentParser(description="Generates a merged mod from several mod folders") @@ -74,11 +74,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" @@ -427,6 +447,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 From 58f9f0c2bcf6b1f873714358afd1a0f93f1ed7f8 Mon Sep 17 00:00:00 2001 From: Artprozew Date: Thu, 14 Dec 2023 15:21:28 -0300 Subject: [PATCH 4/4] Feat: Add option to merge weapons Add choice to skip possible unused INIs --- Tools/genshin_merge_mods.py | 87 ++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/Tools/genshin_merge_mods.py b/Tools/genshin_merge_mods.py index 97c48b7..1054c0b 100644 --- a/Tools/genshin_merge_mods.py +++ b/Tools/genshin_merge_mods.py @@ -23,6 +23,7 @@ import sys import traceback + def main(): parser = argparse.ArgumentParser(description="Generates a merged mod from several mod folders") parser.add_argument("-r", "--root", type=str, default=".", help="Location to use to create mod") @@ -33,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() @@ -106,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 = [] @@ -140,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] @@ -163,11 +143,27 @@ def main(): 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 @@ -189,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 @@ -198,7 +198,6 @@ 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"]: @@ -206,10 +205,10 @@ def main(): 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"]]})?') - response = input('(Y/N | Skip): ') - if response.lower() == "y": + choice = input('(Y/N | Skip): ') + if choice.lower() == "y": continue - elif response.lower() == "skip": + elif choice.lower() == "skip": skip = all_mod_data[i]['ini_group'] break else: @@ -224,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: @@ -304,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 @@ -324,8 +349,10 @@ 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: + 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): @@ -365,7 +392,7 @@ def collect_ini(path, ignore): 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()):