Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 123 additions & 33 deletions Tools/genshin_merge_mods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()

Expand Down Expand Up @@ -72,38 +76,40 @@ 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"
overrides = "; Overrides ---------------------------\n\n"
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 = []
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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()):
Expand Down Expand Up @@ -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')