diff options
| author | 2026-04-02 21:58:50 -0400 | |
|---|---|---|
| committer | 2026-04-02 21:58:50 -0400 | |
| commit | da80badc0e0acaa11459aefeb486f7e58c473eb3 (patch) | |
| tree | 519e9ae648675fad3a9dae05895ca4f8731037e4 | |
| parent | 9bebe7aa2ea80df948338e8b8cee256f7b6454d9 (diff) | |
| download | NewCamera-da80badc0e0acaa11459aefeb486f7e58c473eb3.tar.gz NewCamera-da80badc0e0acaa11459aefeb486f7e58c473eb3.tar.bz2 NewCamera-da80badc0e0acaa11459aefeb486f7e58c473eb3.zip | |
ModSlots better extract logic, add shuffle
Signed-off-by: Andrew Opalach <andrew@akon.city>
| -rw-r--r-- | ModSlots/ArmorIDs.py | 3 | ||||
| -rwxr-xr-x | ModSlots/ModSlots.py | 211 | ||||
| -rw-r--r-- | ModSlots/slots.example.txt | 177 |
3 files changed, 289 insertions, 102 deletions
diff --git a/ModSlots/ArmorIDs.py b/ModSlots/ArmorIDs.py index 1863f36..d37af35 100644 --- a/ModSlots/ArmorIDs.py +++ b/ModSlots/ArmorIDs.py @@ -48,5 +48,6 @@ ArmorIDs = { 'Brigade': ['pl036_0000', 'slg001_0000'], 'Diver': ['pl075_0000', 'slg000_0000'], 'Azure Starlord': ['pl072_0000', 'slg072_0000'], - 'Azure Age': ['pl072_0001', 'slg072_0001'] + 'Azure Age': ['pl072_0001', 'slg072_0001'], + 'Vaal Hazak (β)': ['pl032_0010', 'slg032_0000'] } diff --git a/ModSlots/ModSlots.py b/ModSlots/ModSlots.py index 5a4f216..6460f4c 100755 --- a/ModSlots/ModSlots.py +++ b/ModSlots/ModSlots.py @@ -6,12 +6,15 @@ import os import os.path import shutil import re +import struct +import time +import random from enum import Enum from hashlib import sha256 from zipfile import ZipFile try: - from unrardll import extract, names + import unrardll RAR_SUPPORT = True except ImportError: RAR_SUPPORT = False @@ -24,6 +27,9 @@ except ImportError: from ArmorIDs import ArmorIDs +def flatten(xss): + return [x for xs in xss for x in xs] + NATIVEPC_SUBDIRS = ('ec', 'hm', 'npc', 'pg', 'posteffect', 'sound', 'ui', 'village', 'accessory', 'em', 'light', 'otomo', 'photo', 'quest', 'system', 'unit_resource', 'common', 'event', 'ngword_list', 'pc', 'pl', 'shortcut', 'vfx', 'wp', 'plugins') USAGE = f'Usage: {sys.argv[0]} <Write/Verify/Delete> <Slots.txt> <Path_to_Mods> <Path_to_Game>' @@ -37,7 +43,6 @@ Slots = sys.argv[2] Mods_Directory = sys.argv[3] Game_Directory = sys.argv[4] Extract_Directory = f'{Mods_Directory}/extract' -Hash_Path = f'{Game_Directory}/mod_slots_hash.txt' List_Path = f'{Game_Directory}/mod_slots.txt' if not os.path.isfile(Slots): @@ -59,8 +64,8 @@ if not os.path.isdir(Game_Directory): print(f'Directory {Game_Directory} doesn\'t exist.') sys.exit(1) -if Mode == 'verify' and not os.path.isfile(Hash_Path): - print(f'Verification requested but previous hash not found at {Hash_Path}.') +if Mode == 'verify' and not os.path.isfile(List_Path): + print(f'Verification requested but previous hash not found at {List_Path}.') sys.exit(1) Error_Messages = [] @@ -68,11 +73,20 @@ Error_Index = 1 def Error(msg): global Error_Messages global Error_Index - msg = f'{Error_Index}: {msg}' + msg = f'E(#{Error_Index}): {msg}' Error_Messages.append(msg) Error_Index += 1 print(msg + '\n' + '^'*len(msg)) +Warning_Index = 1 +def Warn(msg): + global Error_Messages + global Warning_Index + msg = f'W(#{Warning_Index}): {msg}' + Error_Messages.append(msg) + Warning_Index += 1 + print(msg + '\n' + '^'*len(msg)) + class ArchiveType(Enum): ZIP = 0 RAR = 1 @@ -80,6 +94,7 @@ class ArchiveType(Enum): class Mod(): def __init__(self, path): + self.path = path self.name = path.split('/')[-1] m = sha256() m.update(open(path, 'rb').read()) @@ -94,17 +109,32 @@ class Mod(): continue self.files.append(file.filename) elif ext == 'rar': - if not SEVEN_ZIP_SUPPORT: - raise Exception("Use of .rar archive without required package (unrardll).") + if not RAR_SUPPORT: + raise Exception('Use of .rar archive without required package (unrardll).') self.type = ArchiveType.RAR + for header in list(unrardll.headers(path)): + if not header['is_dir']: + self.files.append(header['filename']) elif ext == '7z': if not SEVEN_ZIP_SUPPORT: - raise Exception("Use of .7z archive without required package (py7zr).") + raise Exception('Use of .7z archive without required package (py7zr).') self.type = ArchiveType.SEVEN_ZIP self.obj = SevenZipFile(path, 'r') for file in self.obj.list(): if not file.is_directory: self.files.append(file.filename) + # Filter paths to fix automatic processing. + self.raw_files = self.files.copy() + for i, file in enumerate(self.files): + npc = file.lower().find('nativepc') + if npc >= 0: + cut = file[npc:npc + len('nativePC')] + if cut != 'nativePC': + print(f'Correcting nativePC capitalization in {file}.') + self.files[i] = file.replace(cut, 'nativePC') + self.raw_map = {} + for i in range(0, len(self.files)): + self.raw_map[self.files[i]] = self.raw_files[i] self.roots = [] self.ignored = [] self.overrides = {} @@ -136,9 +166,9 @@ class Mod(): def maybe_strip_root(self, file): for root in self.roots: if file.startswith(root): - nativePC = file.find('nativePC') - if nativePC >= 0: - return file[nativePC:], root + npc = file.find('nativePC') + if npc >= 0: + return file[npc:], root return file, None def do_map(self, All_Files, Global_Overrides): @@ -163,7 +193,8 @@ class Mod(): if len(target.split('/')[-1].split('.')) == 1: print(f'Warning: Possible non-direct path as a direct override: {target}') targets.append(target) - elif not has_global_override: # Could switch to else. + # Add file as-is unless it could override an explicit target of a different file. + elif base not in flatten(self.overrides.values()) and not has_global_override: targets.append(base) # Substitute armor names, like '[Anja]', with their IDs. for i, target in enumerate(targets): @@ -217,43 +248,52 @@ class Mod(): output = f'{os.path.join(Extract_Directory, self.hash)}' if not os.path.isdir(output): os.mkdir(output) + do_extract = False + on_disk = [os.path.join(root, file) for root, dirs, files in os.walk(output) for file in files] + for key in self.map: + file = os.path.join(output, self.raw_map[key]) + if file not in on_disk: + do_extract = True + break + if do_extract: if self.type == ArchiveType.ZIP: self.obj.extractall(path=output) + elif self.type == ArchiveType.RAR: + unrardll.extract(self.path, output) elif self.type == ArchiveType.SEVEN_ZIP: self.obj.extractall(path=output) for key in self.map: - m.update(open(os.path.join(output, key), 'rb').read()) + m.update(open(os.path.join(output, self.raw_map[key]), 'rb').read()) - def write(self, old_list): + def write(self, old_list, list_file): output = f'{os.path.join(Extract_Directory, self.hash)}' - with open(List_Path, 'a+') as l: - for key in self.map: - targets = self.map[key] - for target in targets: - copy = target[0] == '|'; - if copy: - target = target[1:] - if target in old_list: - old_list.remove(target) - l.write(target + '\n') - inc = Game_Directory - for s in target.split('/')[:-1]: - inc = os.path.join(inc, s) - if not os.path.isdir(inc): - os.mkdir(inc) - target = os.path.join(Game_Directory, target) - if os.path.islink(target): - os.remove(target) - elif os.path.isfile(target) or os.path.isdir(target): - if not copy: - Error(f'Not replacing/removing non-symlink file: {target}') - continue - file = os.path.join(output, key) - if copy: - print(f'{file}\n (detached) -> {target}') - shutil.copyfile(file, target) - else: - os.symlink(file, target) + for key in self.map: + targets = self.map[key] + for target in targets: + copy = target[0] == '|'; + if copy: + target = target[1:] + if target in old_list: + old_list.remove(target) + list_file.write(target + '\n') + inc = Game_Directory + for s in target.split('/')[:-1]: + inc = os.path.join(inc, s) + if not os.path.isdir(inc): + os.mkdir(inc) + target = os.path.join(Game_Directory, target) + if os.path.islink(target): + os.remove(target) + elif os.path.isfile(target) or os.path.isdir(target): + if not copy: + Warn(f'Not replacing/removing non-symlink file: {target}') + continue + file = os.path.join(output, self.raw_map[key]) + if copy: + print(f'{file}\n (detached) -> {target}') + shutil.copyfile(file, target) + else: + os.symlink(file, target) def delete(self): for key in self.map: @@ -263,7 +303,7 @@ class Mod(): if os.path.islink(target): os.remove(target) elif os.path.isfile(target) or os.path.isdir(target): - Error(f'Not deleting non-symlink file: {target}') + Warn(f'Not deleting non-symlink file: {target}') def is_armor_override(line): sp = line.split('/') @@ -275,20 +315,62 @@ def is_armor_override(line): return True return False +Seed = time.time() +if Mode == 'write': + random.seed(Seed) + +with open(List_Path, 'a+') as l: + l.seek(0) + old_list = l.read().splitlines() + if len(old_list) > 2: + old_hash = old_list[0] + try: + old_seed = struct.unpack('!d', bytes.fromhex(old_list[1]))[0] + except ValueError: + Error(f'Invalid seed \'{old_list[1]}\' in {List_Path}.') + else: + if Mode == 'verify': + random.seed(old_seed) + old_list = old_list[2:] + if Mode == 'write': + l.truncate(0) + Mods = [] +Shuffle = None Current_Mod = None Current_Override = None Global_Overrides = {} with open(Slots, 'r') as f: - for line in f.read().splitlines(): + for ln, line in enumerate(f.read().splitlines()): if len(line) == 0 or line[0] == '#': continue if line == 'XXX': break - if line[0] == ';': + if line[0] in [';', 's']: if Current_Mod: Mods.append(Current_Mod) Current_Mod = None + if line[0] == 's': + Shuffle = [] + path = os.path.join(Mods_Directory, line[1:]) + if os.path.isfile(path): + Shuffle.append(path) + else: + Error(f'File not found: {path}') + continue + elif line[0] == '+': + path = os.path.join(Mods_Directory, line[1:]) + if os.path.isfile(path): + Shuffle.append(path) + else: + Error(f'File not found: {path}') + continue + elif line[0:4].upper() == 'ROLL': + Current_Mod = Mod(random.choice(Shuffle)) + Current_Override = None + Shuffle = None + continue + if line[0] == ';': path = os.path.join(Mods_Directory, line[1:]) if os.path.isfile(path): Current_Mod = Mod(path) @@ -296,7 +378,7 @@ with open(Slots, 'r') as f: else: Error(f'File not found: {path}') continue - cmd = '' + cmd = None if line[0] == '&': cmd = 'root' elif line[0] == '<': @@ -313,16 +395,21 @@ with open(Slots, 'r') as f: line = line[1:] if line.startswith(NATIVEPC_SUBDIRS): line = 'nativePC/' + line + else: + continue if cmd == 'root' and Current_Mod: Current_Mod.roots.append(line) elif cmd == 'ignore' and Current_Mod: Current_Mod.ignored.append(line) elif cmd == 'detach' and Current_Mod: - Current_Mod.ignored.append(line) - Current_Mod.map[line] = [f'|{line}'] + if line not in Current_Mod.files: + Error(f'File not found: {Current_Mod.name}:{line}') + else: + Current_Mod.ignored.append(line) + Current_Mod.map[line] = [f'|{line}'] elif cmd == 'override' and Current_Mod: if not (is_armor_override(line) or line in Current_Mod.files): - Error(f'File not found: {line}') + Error(f'File not found: {Current_Mod.name}:{line}') else: Current_Override = line Current_Mod.overrides[Current_Override] = [] @@ -353,29 +440,23 @@ for mod in Mods: mod.do_map(All_Files, Global_Overrides) mod.add_to_hash(M) -with open(List_Path, 'a+') as l: - l.seek(0) - old_list = l.read().splitlines() - if Mode == 'write': - l.truncate(0) - if Mode == 'write': - for mod in Mods: - mod.write(old_list) + with open(List_Path, 'a+') as l: + l.write(M.hexdigest() + '\n') + l.write(hex(struct.unpack('!Q', struct.pack('!d', Seed))[0])[2:] + '\n') + for mod in Mods: + mod.write(old_list, l) for old in old_list: old_target = os.path.join(Game_Directory, old) if os.path.islink(old_target): print(f'Removing old link: {old_target}') os.remove(old_target) - with open(Hash_Path, 'w+') as f: - f.write(M.hexdigest()) + if Mode == 'verify': - if os.path.isfile(Hash_Path): - with open(Hash_Path, 'r') as f: - if f.read() == M.hexdigest(): - print('Verification passed!') - else: - print('Verification failed.') + if old_hash == M.hexdigest(): + print('Verification passed!') + else: + print('Verification failed.') elif Mode == 'delete': for mod in Mods: mod.delete() diff --git a/ModSlots/slots.example.txt b/ModSlots/slots.example.txt index 76cb440..06de84d 100644 --- a/ModSlots/slots.example.txt +++ b/ModSlots/slots.example.txt @@ -12,18 +12,23 @@ <plugins/QuestLoader.dll :dinput8.dll :loader.dll -:loader-config.json +# Copy loader-config.json instead of linking it so any edits save to the game directory not the extracted mod folder. +|loader-config.json ;SharpPluginLoader-0.0.7.2-linux.zip :msvcrt.dll +# DPSTickFix +#;Fatalis Patch-3474-1-4-1601540952.zip +# BetterInputs +#;Fatalis Patch-4333-1-4-1601540927.zip + ;2024 Patch 15.23 1.25-3473-1-25-1728572867.zip ;MHWI - Hide main menu ad banner-7826-1-0-1728933976.zip -;MHWNewCamera_3.0.zip -# Copy MHWNewCamera.json instead of symlink so our edits save to the game directory not the extracted mod folder. -|plugins/CSharp/MHWNewCamera.json +#;MHWNewCamera_3.0.zip +#|plugins/CSharp/MHWNewCamera.json ;Fix_Tent_Animation_Face_Deform_v2.zip @@ -42,35 +47,38 @@ XXX :pl/f_equip/pl002_0000/wst/mod/Unshaved.mod3 >pl/f_equip/pl002_0000/wst/mod/f_wst002_0000.mod3 -;Version1.0 Normal Fishnet for Highpoly Nude MOD Version 4-5030-1-0-1610636569.zip -<pl/f_equip/pl002_0000/body -:pl/f_equip/pl002_0000/arm ->[Hornetaur] -:pl/f_equip/pl002_0000/wst ->[Hornetaur] -:pl/f_equip/pl002_0000/leg ->[Hornetaur] +#;Version1.0 Normal Fishnet for Highpoly Nude MOD Version 4-5030-1-0-1610636569.zip +#<pl/f_equip/pl002_0000/body +#:pl/f_equip/pl002_0000/arm +#>[Hornetaur] +#:pl/f_equip/pl002_0000/wst +#>[Hornetaur] +#:pl/f_equip/pl002_0000/leg +#>[Hornetaur] -;Ver1.0 Pantie for Highpoly Nude MOD-3465-1-0-1589948707.zip +#;Ver1.0 Pantie for Highpoly Nude MOD-3465-1-0-1589948707.zip +;Ver2.0 HPNM V4 Pantie for Highpoly Nude MOD-3465-2-0-1605825156.rar <pl/f_equip/HPNM_KirinPantie/CMM_Normal.tex <pl/f_equip/HPNM_KirinPantie/CMM_Stripe.tex <pl/f_equip/HPNM_KirinPantie/CMM_PolkaDot.tex -:Pantie_ColorMaskMap/Translucent/CMM_TransVKS_Pantie_Palico.mrl3 +:Pantie_ColorMaskMap/CMM_Pantie_Palico.mrl3 >pl/f_equip/pl002_0000/wst/mod/f_wst002_0000.mrl3 :pl/f_equip/pl002_0000/wst >[Kirin] -;Ver3.1 SmallTitsVer_Highpoly Nude MOD with jiggle animation (Iceborne Compatible )-1965-3-1-1586846362.zip -<pl/f_equip/HighPolyNudeModTextures/ColorUnchangeable_Nail_CMM.tex -<pl/f_equip/HighPolyNudeModTextures/Nail_BML.tex -<pl/f_equip/HighPolyNudeModTextures/Nail_CMM.tex -<pl/f_equip/HighPolyNudeModTextures/Nail_Disable_snow_Col_CMM.tex -<pl/f_equip/HighPolyNudeModTextures/Nail_NM.tex -<pl/f_equip/HighPolyNudeModTextures/Nail_RMT.tex -#:pl/f_equip/pl501_0000/body/mod/f_body501_0000.ctc -#*pl/f_equip/pl[0-9]+_[0-9]+/body/mod/f_body[0-9]+_[0-9]+.ctc -#:pl/f_equip/pl501_0000/leg/mod/f_leg501_0000.ctc -#*pl/f_equip/pl[0-9]+_[0-9]+/leg/mod/f_leg[0-9]+_[0-9]+.ctc +;Ver4.2 Small_Highpoly Nude MOD with jiggle animation-1965-4-2-1607065917.rar +#;Ver3.1 SmallTitsVer_Highpoly Nude MOD with jiggle animation (Iceborne Compatible )-1965-3-1-1586846362.zip +<pl/f_face/ +#<pl/f_equip/HighPolyNudeModTextures/ColorUnchangeable_Nail_CMM.tex +#<pl/f_equip/HighPolyNudeModTextures/Nail_BML.tex +#<pl/f_equip/HighPolyNudeModTextures/Nail_CMM.tex +#<pl/f_equip/HighPolyNudeModTextures/Nail_Disable_snow_Col_CMM.tex +#<pl/f_equip/HighPolyNudeModTextures/Nail_NM.tex +#<pl/f_equip/HighPolyNudeModTextures/Nail_RMT.tex +##:pl/f_equip/pl501_0000/body/mod/f_body501_0000.ctc +##*pl/f_equip/pl[0-9]+_[0-9]+/body/mod/f_body[0-9]+_[0-9]+.ctc +##:pl/f_equip/pl501_0000/leg/mod/f_leg501_0000.ctc +##*pl/f_equip/pl[0-9]+_[0-9]+/leg/mod/f_leg[0-9]+_[0-9]+.ctc #;Atrarch's Namielle For Dytsers Armor's-4490-4-0-1630065481.zip #<pl/f_equip/pl @@ -86,11 +94,11 @@ XXX #:4k face textures/f_face_edit_NM.tex #>pl/f_face/face000/mod/f_face_edit_NM.tex -;Skin Glossiness Options.zip -:skin_BM_dry.tex ->pl/f_equip/mangie_foam/skin_BM.tex ->pl/f_equip/mangie_fortune/skin_BM.tex -*pl/f_equip/mangie/.*/skin.*_BM.tex +#;Skin Glossiness Options.zip +#:skin_BM_dry.tex +#>pl/f_equip/mangie_foam/skin_BM.tex +#>pl/f_equip/mangie_fortune/skin_BM.tex +#*pl/f_equip/mangie/.*/skin.*_BM.tex ;Bikini Collections - Barefoot-2891-1-0-1584527076.zip <pl/f_equip/pl @@ -108,8 +116,8 @@ XXX ;Lewd Lingerie.zip >[Jyura] -;Secret Report.zip ->[Brigade] +#;Secret Report.zip +#>[Brigade] ;Eternal Summer.zip # The uncensored files will be used with this order. @@ -144,14 +152,111 @@ XXX ;Dark Widow.zip >[Jyura+] -;DOAXVV Angel_s Smile.zip ->[Diver] - ;Lake Elven.zip >[Azure Age] ;DOAXVV Foam Bikini.zip >[Jagras] -;Pistachio.zip +;DOAXVV Cure Angel.zip >[Jagras+] + +sBamboo Shoot.zip ++White Prince.zip ++Kaine.zip ++Nyo Latex.zip ++DOA6 Tamaki Bikini.zip ++Hebijo Shinobi Costume.zip ++Honoka Newcomer Costume.zip ++Leifang Yin Yang.zip ++Poll Winner - Black Rubber.zip ++Swimsuit.zip ++Sailor Outfit.zip ++Queen Marika.zip ++Bunny Girl Costume 3.zip ++Sailor Swimsuit.zip ++Desert Costume.zip ++2nd Design Contest Swimsuit.zip ++DOA6 Bunny Girl Costumes.zip ++DOA5LR Kasumi_s Battlesuit.zip ++Rabbit Joker.zip ++Princess Warrior.zip ++Pub Lass Bunny Girl Costume.zip ++Pistachio.zip ++DOA5LR Momiji Kimono.zip ++Stellar Scorpion.zip ++Destiny Child Mona Costume.zip ++Lingerie.zip ++Demonlord.zip ++Secret Report.zip ++DOAXVV Angel_s Smile.zip ++Tifa Mature Outfit.zip ++DOA5LR Rachel_s Halloween Costume.zip ++Bunny Girl Costume 1.zip ++2023 Halloween Costume.zip ++DOA6 Leather Jacket.zip ++DOAXVV Martini.zip ++Dark Widow.zip ++Fraise Noel.zip ++Nyo Pirate.zip ++Code Rouge.zip ++Venus Valkyrie.zip ++Venus Nine.zip ++Noble Tutu.zip ++Black Lily.zip ++DOA5LR Christie Halloween Costume.zip ++Endorphine Sky.zip ++DOAXVV Fortune.zip ++White Summer Bikini.zip ++Ice Dress.zip ++Lewd Sailor.zip ++Bunny Girl Costume 2.zip ++Peace.zip ++Rachel Crop Top.zip ++Marionette.zip ++Luminate Tube.zip ++Kunimitsu Outfit.zip ++Mikomai.zip ++Venus Cage.zip ++Lake Elven.zip ++Mood For Love.zip ++DOAXVV Foam Bikini.zip ++Aventure.zip ++Dragon Leotard.zip ++Christie_s Elegance Dress.zip ++DOAXVV Noctilca.zip ++Crimson Sakura.zip ++Nyotengu_s Halloween Costume.zip ++Momiji Costume.zip ++Fur Bikini.zip ++Claire Noir.zip ++DOAXVV Cure Angel.zip ++Auspicious Blossom.zip ++Momo Denim.zip ++Appetizer Shrimp.zip ++Ada Wong Dress.zip ++Christmas Gift.zip ++Jewel Topaz.zip ++DOA5LR Leifang_s Black Skirt.zip ++Hinode.zip ++Alluring Mandarin.zip ++Neoncrow.zip ++1st Swimsuit Contest.zip ++DOA6 Kunoichi Costumes (Ayane, Kasumi).zip ++Rachel Fiend Costume.zip ++Midnight Light.zip ++DOA6 Christie_s Costume.zip ++DOAXVV Velvet Time Pearl.zip ++Crystal.zip ++Kunoichi - Kasumi.zip ++Oppression.zip ++DOAXVV Asari.zip ++DOAXVV Narwhal.zip ++DOA6 Summer Breeze.zip ++Kasumi Award Outfit.zip ++Succubus Damaged.zip ++Lewd Lingerie.zip ++Kunoichi - Ayane.zip ++Nier Automata YoRHa 2B.zip +ROLL +>[Diver] |