diff options
Diffstat (limited to 'ModSlots')
| -rw-r--r-- | ModSlots/ArmorIDS.py | 355 | ||||
| -rwxr-xr-x | ModSlots/ModSlots.py | 261 | ||||
| -rw-r--r-- | ModSlots/cmd.txt | 1 | ||||
| -rw-r--r-- | ModSlots/requirements.txt | 2 |
4 files changed, 619 insertions, 0 deletions
diff --git a/ModSlots/ArmorIDS.py b/ModSlots/ArmorIDS.py new file mode 100644 index 0000000..8526ab6 --- /dev/null +++ b/ModSlots/ArmorIDS.py @@ -0,0 +1,355 @@ +# https://github.com/Ezekial711/MonsterHunterWorldModding/wiki/Armor-IDs +# Macro reformat, Remove None/Unavailable, sort u +# Duplicates: +# I guess this means the list has lots of errors, unless I'm missing something. +# It would take a while to verify all this. +''' + 'pl001_0000': 'Skull', + 'pl001_0000': 'Bushi "Homare"', + 'pl001_0000': 'Bushi "Sabi"', + 'pl001_0000': 'Unavailable', + 'pl002_0000': 'Hunter', + 'pl002_0010': 'Hunter', + 'pl003_0000': 'Bone', + 'pl004_0000': 'Alloy', + 'pl005_0000': 'Kestodon', + 'pl006_0000': 'Gajau', + 'pl009_0000': 'Kulu', + 'pl009_0001': 'Kulu+', + 'pl010_0000': 'Pukei', + 'pl010_0001': 'Pukei+', + 'pl010_0010': 'Pukei', + 'pl011_0000': 'Jyura', + 'pl011_0001': 'Jyura+', + 'pl013_0000': 'Kadachi', + 'pl013_0001': 'Kadachi+', + 'pl014_0000': 'Chain', + 'pl015_0000': 'Gastodon', + 'pl016_0000': 'Barnos', + 'pl019_0000': 'Butterfly', + 'pl020_0000': 'Anja', + 'pl020_0001': 'Anja+', + 'pl020_0010': 'Anja', + 'pl020_0011': 'Anja+', + 'pl022_0000': 'Girros', + 'pl022_0001': 'Girros+', + 'pl023_0000': 'Tzitzi', + 'pl023_0001': 'Tzitzi+', + 'pl024_0000': 'Lumu', + 'pl024_0001': 'Lumu+', + 'pl025_0000': 'High Metal', + 'pl028_0000': 'Baan', + 'pl028_0001': 'Baan+', + 'pl028_0010': 'Baan', + 'pl031_0000': 'Ingot', + 'pl037_0000': 'Unavailable', + 'pl038_0000': 'Zorah', + 'pl038_0010': 'Zorah', + 'pl042_0000': 'Bazel', + 'pl042_0010': 'Bazel', + 'pl043_0000': 'Damascus', + 'pl043_0010': 'Damascus', + 'pl044_0000': 'Dober', + 'pl045_0000': 'Xeno', + 'pl045_0010': 'Xeno', + 'pl045_0020': 'Xeno', + 'pl049_0000': 'Kulve Taroth', + 'pl049_0010': 'Kulve Taroth', + 'pl049_0020': 'Kulve', + 'pl054_0000': 'Skull', + 'pl057_0000': 'Butterfly', + 'pl058_0000': 'Shamos', + 'pl059_0000': 'Bushi "Homare"', + 'pl060_0000': 'Strategist', + 'pl061_0000': 'Dragonking', + 'pl072_0001': 'Azure Age', + 'pl091_0000': 'Artian', + 'pl093_0000': 'Black Belt', + 'pl095_0000': 'Unavailable', + 'pl095_0010': 'Unavailable', + 'pl098_0000': 'Duffel Penguin+', + 'pl100_0000': 'Sealed Dragon Cloth', + 'pl101_0000': 'Wyverian Ears', + 'pl110_0000': 'Beo+', + 'pl118_0000': 'Geralt', + 'pl121_0000': 'Guardian', + 'pl121_0000': 'Guardian+', + 'pl124_0000': 'Rose', + 'pl125_0000': 'Passion', + 'pl126_0000': 'Demonlord', + 'pl127_0000': 'Oolong', + 'pl128_0000': 'Astral', +''' + +ArmorIDS = { + 'pl001_0000': 'Leather', + 'pl002_0000': 'Hunter\'s', + 'pl002_0010': 'Hunter\'s', + 'pl003_0000': 'Bone+', + 'pl004_0000': 'Alloy+', + 'pl005_0000': 'Kestodon+', + 'pl006_0000': 'Gajau+', + 'pl007_0000': 'Vespoid', + 'pl007_0001': 'Vespoid+', + 'pl008_0000': 'Jagras', + 'pl008_0001': 'Jagras+', + 'pl009_0000': 'Kulu-Ya-Ku', + 'pl009_0001': 'Kulu-Ya-Ku+', + 'pl010_0000': 'Pukei-Pukei', + 'pl010_0001': 'Pukei-Pukei+', + 'pl010_0010': 'Pukei-Pukei', + 'pl010_0101': 'Coral Pukei+', + 'pl011_0000': 'Jyuratodus', + 'pl011_0001': 'Jyuratodus+', + 'pl012_0000': 'Barroth', + 'pl012_0001': 'Barroth+', + 'pl013_0000': 'Tobi-Kadachi', + 'pl013_0001': 'Tobi-Kadachi+', + 'pl013_0101': 'Viper Kadachi+', + 'pl014_0000': 'Chainmail', + 'pl015_0000': 'Gastodon+', + 'pl016_0000': 'Barnos+', + 'pl017_0000': 'Hornetaur', + 'pl017_0001': 'Hornetaur+', + 'pl018_0000': 'Origin', + 'pl019_0000': 'King Beetle', + 'pl020_0000': 'Anjanath', + 'pl020_0001': 'Anjanath+', + 'pl020_0010': 'Anjanath', + 'pl020_0011': 'Anjanath+', + 'pl020_0101': 'Fulgur Anja+', + 'pl020_0111': 'Fulgur Anja+', + 'pl021_0000': 'Rathian', + 'pl021_0001': 'Rathian+', + 'pl021_0010': 'Rathian', + 'pl021_0011': 'Rathian+', + 'pl021_0100': 'Rath Heart', + 'pl021_0101': 'Rath Heart+', + 'pl021_0110': 'Rath Heart', + 'pl021_0111': 'Rath Heart+', + 'pl021_0200': 'Golden Lune+', + 'pl021_0210': 'Golden Lune+', + 'pl022_0000': 'Great Girros', + 'pl022_0001': 'Great Girros+', + 'pl023_0000': 'Tzitzi-Ya-Ku', + 'pl023_0001': 'Tzitzi-Ya-Ku+', + 'pl024_0000': 'Paolumu', + 'pl024_0001': 'Paolumu+', + 'pl024_0101': 'Lumu Phantasm+', + 'pl025_0000': 'High Metal+', + 'pl026_0000': 'Death Stench', + 'pl027_0000': 'Legiana', + 'pl027_0001': 'Legiana+', + 'pl027_0010': 'Legiana', + 'pl027_0011': 'Legiana+', + 'pl027_0500': 'Shrieking Legia+', + 'pl027_0510': 'Shrieking Legia+', + 'pl028_0000': 'Radobaan', + 'pl028_0001': 'Radobaan+', + 'pl028_0010': 'Radobaan', + 'pl029_0000': 'Odogaron', + 'pl029_0001': 'Odogaron+', + 'pl029_0010': 'Odogaron', + 'pl029_0011': 'Odogaron+', + 'pl029_0101': 'Death Garon+', + 'pl029_0111': 'Death Garon+', + 'pl030_0000': 'Nergigante', + 'pl030_0010': 'Nergigante', + 'pl030_0020': 'Nergigante', + 'pl030_0500': 'Ruiner Nergi+', + 'pl030_0510': 'Ruiner Nergi+', + 'pl031_0000': 'Ingot+', + 'pl032_0000': 'Vaal Hazak', + 'pl032_0010': 'Vaal Hazak', + 'pl032_0020': 'Vaal Hazak', + 'pl032_0500': 'Blackveil Hazak+', + 'pl032_0510': 'Blackveil Hazak+', + 'pl033_0000': 'Rathalos', + 'pl033_0001': 'Rathalos+', + 'pl033_0010': 'Rathalos', + 'pl033_0011': 'Rathalos+', + 'pl033_0100': 'Rath Soul', + 'pl033_0101': 'Rath Soul+', + 'pl033_0110': 'Rath Soul', + 'pl033_0111': 'Rath Soul+', + 'pl033_0200': 'Silver Sol+', + 'pl033_0210': 'Silver Sol+', + 'pl034_0000': 'Diablos', + 'pl034_0001': 'Diablos+', + 'pl034_0010': 'Diablos', + 'pl034_0011': 'Diablos+', + 'pl034_0100': 'Diablos Nero', + 'pl034_0101': 'Diablos Nero+', + 'pl034_0110': 'Diablos Nero', + 'pl034_0111': 'Diablos Nero+', + 'pl035_0000': 'Kirin', + 'pl035_0001': 'Kirin+', + 'pl035_0010': 'Kirin', + 'pl035_0011': 'Kirin+', + 'pl035_0020': 'Kirin', + 'pl036_0000': 'Brigade', + 'pl037_0000': 'Pulverizing Feather', + 'pl038_0000': 'Zorah+', + 'pl038_0010': 'Zorah+', + 'pl038_0020': 'Zorah', + 'pl039_0000': 'Dodogama', + 'pl039_0001': 'Dodogama+', + 'pl040_0000': 'Lavasioth', + 'pl040_0001': 'Lavasioth+', + 'pl040_0010': 'Lavasioth', + 'pl041_0000': 'Uragaan', + 'pl041_0001': 'Uragaan+', + 'pl041_0010': 'Uragaan', + 'pl042_0000': 'Bazelgeuse', + 'pl042_0010': 'Bazelgeuse', + 'pl042_0500': 'Seething Bazel+', + 'pl042_0510': 'Seething Bazel+', + 'pl043_0000': 'Damascus+', + 'pl043_0010': 'Damascus+', + 'pl044_0000': 'Dober+', + 'pl045_0000': 'Xeno\'jiiva', + 'pl045_0010': 'Xeno\'jiiva', + 'pl045_0020': 'Xeno\'jiiva', + 'pl046_0000': 'Teostra', + 'pl046_0001': 'Teostra+', + 'pl046_0010': 'Teostra', + 'pl046_0011': 'Teostra+', + 'pl046_0020': 'Teostra', + 'pl047_0000': 'Kushala', + 'pl047_0001': 'Kushala+', + 'pl047_0010': 'Kushala', + 'pl047_0011': 'Kushala+', + 'pl047_0020': 'Kushala', + 'pl048_0000': 'Guild Cross', + 'pl049_0000': 'Kulve', + 'pl049_0001': 'Kulve Taroth+', + 'pl049_0010': 'Kulve', + 'pl049_0011': 'Kulve Taroth+', + 'pl049_0020': 'Kulve Taroth', + 'pl050_0000': 'Deviljho', + 'pl050_0010': 'Deviljho', + 'pl050_0500': 'Savage Jho+', + 'pl050_0510': 'Savage Jho+', + 'pl051_0000': 'Lunastra', + 'pl051_0001': 'Lunastra+', + 'pl051_0010': 'Lunastra', + 'pl051_0011': 'Lunastra+', + 'pl051_0020': 'Lunastra', + 'pl052_0000': 'Alatreon+', + 'pl052_0010': 'Alatreon+', + 'pl053_0000': 'Commission', + 'pl054_0000': 'Skull Mask', + 'pl055_0000': 'Faux Felyne', + 'pl056_0000': 'Mosswine Mask', + 'pl057_0000': 'Queen Beetle', + 'pl057_0010': 'Butterfly', + 'pl058_0000': 'Shamos+', + 'pl059_0000': 'Crystal Earring', + 'pl060_0000': 'Strategist Spectacles', + 'pl061_0000': 'Dragonking Eyepatch', + 'pl062_0000': 'Shadow Shades', + 'pl063_0000': 'Sealed Eyepatch', + 'pl064_0000': 'Wiggler Head', + 'pl065_0000': 'Kulu-Ya-Ku Head', + 'pl066_0000': 'Bushi "Sabi"', + 'pl066_0010': 'Bushi "Homare"', + 'pl067_0000': 'Samurai', + 'pl068_0000': 'HARDUMMY', + 'pl068_0010': 'HARDUMMY', + 'pl068_0020': 'HARDUMMY', + 'pl069_0000': 'Ryu', + 'pl070_0000': 'Sakura', + 'pl071_0000': 'Dante', + 'pl072_0000': 'Azure Starlord', + 'pl072_0001': 'Azure Age+', + 'pl073_0000': 'Drachen', + 'pl074_0000': 'Blossom', + 'pl075_0000': 'Diver', + 'pl076_0000': 'Harvest', + 'pl077_0000': 'Orion', + 'pl078_0000': 'Gala Suit', + 'pl080_0000': 'Nargacuga+', + 'pl080_0010': 'Nargacuga+', + 'pl081_0000': 'Glavenus+', + 'pl081_0010': 'Glavenus+', + 'pl081_0100': 'Acidic Glavenus+', + 'pl081_0110': 'Acidic Glavenus+', + 'pl082_0000': 'Brachydios+', + 'pl082_0010': 'Brachydios+', + 'pl082_0500': 'Raging Brachy+', + 'pl082_0510': 'Raging Brachy+', + 'pl083_0000': 'Tigrex+', + 'pl083_0010': 'Tigrex+', + 'pl083_0100': 'Brute Tigrex+', + 'pl083_0110': 'Brute Tigrex+', + 'pl084_0000': 'Banbaro+', + 'pl084_0010': 'Banbaro+', + 'pl085_0000': 'Velkhana+', + 'pl085_0010': 'Velkhana+', + 'pl085_0020': 'Velkhana+', + 'pl086_0000': 'Namielle+', + 'pl086_0010': 'Namielle+', + 'pl086_0020': 'Namielle+', + 'pl087_0000': 'Safi\'jiiva+', + 'pl087_0010': 'Safi\'jiiva+', + 'pl088_0000': 'Shara Ishvalda+', + 'pl088_0010': 'Shara Ishvalda+', + 'pl089_0000': 'Barioth+', + 'pl089_0010': 'Barioth+', + 'pl089_0500': 'Frostfang Barioth+', + 'pl089_0510': 'Frostfang Barioth+', + 'pl091_0000': 'Artian+', + 'pl092_0000': 'Guildwork+', + 'pl093_0000': 'Black Belt+', + 'pl094_0000': 'Guild Palace+', + 'pl095_0000': 'Acrobat', + 'pl095_0010': 'Showman', + 'pl096_0000': 'Faux Kelbi', + 'pl097_0000': 'Faux Aptonoth', + 'pl098_0000': 'Duffel Penguin Mask+', + 'pl099_0000': 'Skull Scarf', + 'pl100_0000': 'Sealed Dragon Cloth+', + 'pl101_0000': 'Wyverian Ears+', + 'pl102_0000': 'Wulg+', + 'pl103_0000': 'Cortos+', + 'pl104_0000': 'Clockwork+', + 'pl105_0000': 'Direwolf+', + 'pl106_0000': 'Thermae', + 'pl107_0000': 'Zinogre+', + 'pl107_0010': 'Zinogre+', + 'pl107_0100': 'Stygian Zin+', + 'pl107_0110': 'Stygian Zin+', + 'pl108_0000': 'Yian Garuga+', + 'pl108_0010': 'Yian Garuga+', + 'pl109_0000': 'Rajang+', + 'pl109_0010': 'Rajang+', + 'pl109_0500': 'Furious Rajang+', + 'pl109_0510': 'Furious Rajang+', + 'pl110_0000': 'Beotodus+', + 'pl111_0000': 'Hare Band', + 'pl112_0000': 'Downy Crake', + 'pl113_0000': 'Pearlspring+', + 'pl113_0001': 'Goldspring Macaque', + 'pl114_0000': 'Kadachi Scarf', + 'pl115_0000': 'Buff Body+', + 'pl115_0001': 'Buff Body+', + 'pl116_0000': 'Beetle', + 'pl117_0000': 'Butterfly', + 'pl118_0000': 'Geralt of Rivia', + 'pl119_0000': 'Ciri', + 'pl120_0000': 'Bayek', + 'pl121_0000': 'Defender', + 'pl122_0000': 'Yukumo', + 'pl123_0000': 'Silver Knight', + 'pl124_0000': 'Rose+', + 'pl125_0000': 'Passion+', + 'pl126_0000': 'Demonlord+', + 'pl127_0000': 'Oolong+', + 'pl128_0000': 'Astral+', + 'pl129_0000': 'Fatalis+', + 'pl129_0010': 'Fatalis+', + 'pl130_0000': 'Leon+', + 'pl131_0000': 'Claire+', + 'pl500_0000': 'Innerwear', + 'pl501_0000': 'Innerwear' +} diff --git a/ModSlots/ModSlots.py b/ModSlots/ModSlots.py new file mode 100755 index 0000000..164fb0e --- /dev/null +++ b/ModSlots/ModSlots.py @@ -0,0 +1,261 @@ +#! /usr/bin/env python3 + +import sys +sys.dont_write_bytecode = True +import os.path +import hashlib +from enum import Enum +from zipfile import ZipFile +#from unrardll import extract, names +#from py7zr import SevenZipFile +from ArmorIDS import ArmorIDS + +USAGE = f'Usage: {sys.argv[0]} <Slots.txt> <Path_to_Mods> <Path_to_Game>' + +if len(sys.argv) < 3: + print(USAGE) + sys.exit(1) + +Slots = sys.argv[1] +Mods_Directory = sys.argv[2] +Game_Directory = sys.argv[3] +Extract_Directory = f'{Mods_Directory}/extract' +Hash_Dir = f'{Game_Directory}/mod_slots_hash.txt' +List_Dir = f'{Game_Directory}/mod_slots.txt' + +if not os.path.isfile(Slots): + print(f'{Slots} not a file.') + sys.exit(1) + +if not os.path.isdir(Mods_Directory): + print(f'Directory {Mods_Directory} doesn\'t exist.') + sys.exit(1) + +if not os.path.isdir(Extract_Directory): + os.mkdir(Extract_Directory) + +if not os.path.isdir(Game_Directory): + print(f'Directory {Game_Directory} doesn\'t exist.') + sys.exit(1) + +ArmorIDS_Inverted = { v: k for k, v in ArmorIDS.items() } + +class ArchiveType(Enum): + ZIP = 0 + RAR = 1 + SEVEN_ZIP = 2 + +class Mod(): + def __init__(self, path): + m = hashlib.sha256() + m.update(open(path, 'rb').read()) + self.hash = m.hexdigest() + ext = path.split('.')[-1].lower() + if ext == 'zip': + self.type = ArchiveType.ZIP + self.obj = ZipFile(path, 'r') + self.files = [] + for file in self.obj.infolist(): + if file.filename[-1] == '/': + continue + self.files.append(file.filename) + elif ext == 'rar': + self.type = ArchiveType.RAR + pass + elif ext == '7z': + self.type = ArchiveType.SEVEN_ZIP + pass + self.toplevels = [] + self.roots = [] + self.ignored = [] + self.overrides = {} + self.map = {} + + def get_id_from_subpath(self, sub): + for armor_id in ArmorIDS.keys(): + armor_id = armor_id[2:] + search = sub.find(armor_id) + if search >= 0: + return sub[search:search + 8] + + def substitute_override_name(self, name): + if name[0] == '[': + name = name[1:-1] + if name in ArmorIDS_Inverted.keys(): + name = ArmorIDS_Inverted[name][2:] + return name + + def file_sort_key(self, x): + for i in range(0, len(self.roots)): + if x.startswith(self.roots[i]): + return i + return 0 + + def do_map(self, all_files): + for file in sorted(self.files, key=self.file_sort_key): + ignore_this = False + for ig in self.ignored: + if file.startswith(ig): + ignore_this = True + break + if ignore_this: + continue + path = file # Absolute path within archive. + for root in self.roots: + if file.startswith(root): + nativePC = file.find('nativePC') + if nativePC >= 0: + file = file[nativePC:] + break + if file in self.overrides.keys(): + targets = self.overrides[file] + elif not (file.startswith('nativePC') or file in self.toplevels): + continue + else: + targets = [file] + for i in range(0, len(targets)): + target = targets[i] + # Substitute armor names with their IDs. + for key in self.overrides.keys(): + if target.startswith(key): + name = self.substitute_override_name(self.overrides[key][0]) # Sub has to be first element. + target = target.replace(self.get_id_from_subpath(target), name) + if target in all_files: + print(f'DUPLICATE MAPPING: {target}.') + del targets[i] + continue + targets[i] = target + all_files.extend(targets) + print(path) + for target in targets: + print(' -> ' + target) + self.map[path] = targets + + def add_to_hash(self, m): + output = f'{os.path.join(Extract_Directory, self.hash)}' + if not os.path.isdir(output): + os.mkdir(output) + if self.type == ArchiveType.ZIP: + self.obj.extractall(path=output) + for key in self.map: + m.update(open(os.path.join(output, key), 'rb').read()) + + def write(self, old_list): + output = f'{os.path.join(Extract_Directory, self.hash)}' + with open(List_Dir, 'a+') as l: + for key in self.map: + targets = self.map[key] + for target in targets: + 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.isfile(target): + os.remove(target) + os.symlink(f'{os.path.join(output, key)}', f'{target}') + + def delete(self): + for key in self.map: + targets = self.map[key] + for target in targets: + target = os.path.join(Game_Directory, target) + if os.path.isfile(target): + os.remove(target) + +def is_armor_override(line): + sp = line.split('/') + if len(sp) >= 3 and sp[2] == 'slg': + return True + if sp[-1] in ['helm', 'body', 'arm', 'wst', 'leg']: + return True + return False + +Mods = [] +Current_Mod = None +Current_Override = None +with open(Slots, 'r') as f: + for line in f.read().splitlines(): + if len(line) == 0 or line[0] == '#': + continue + if line[0] == ';': + if Current_Mod: + Mods.append(Current_Mod) + Current_Mod = None + path = os.path.join(Mods_Directory, line[1:]) + if os.path.isfile(path): + Current_Mod = Mod(path) + else: + print(f'FILE NOT FOUND: {path}') + continue + cmd = '' + if line[0] == '*': + cmd = 'root' + elif line[0] == '<': + cmd = 'ignore' + elif line[0] == ':': + cmd = 'override' + elif line[0] == '>': + cmd = 'value' + if cmd: + line = line[1:] + if line.startswith('pl/'): + line = 'nativePC/' + line + if line.startswith('wp/'): + line = 'nativePC/' + line + if cmd == 'root' and Current_Mod: + Current_Mod.roots.append(line) + elif cmd == 'ignore' and Current_Mod: + Current_Mod.ignored.append(line) + elif cmd == 'override' and Current_Mod: + if not (is_armor_override(line) or line in Current_Mod.files): + print(f'FILE NOT FOUND: {line}') + Current_Mod = None + continue + Current_Override = line + Current_Mod.overrides[Current_Override] = [] + elif cmd == 'value' and Current_Mod: + if Current_Override: + Current_Mod.overrides[Current_Override].append(line) + else: + Current_Mod.toplevels.append(line) + if Current_Mod: + Mods.append(Current_Mod) + Current_Mod = None + +Files = [] +M = hashlib.sha256() +for mod in Mods: + mod.do_map(Files) + mod.add_to_hash(M) + +with open(List_Dir, 'a+') as l: + l.seek(0) + old_list = l.read().splitlines() + l.truncate(0) + +Mode = 'Write' +if Mode == 'Write': + for mod in Mods: + mod.write(old_list) + 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_Dir, 'w+') as f: + f.write(M.hexdigest()) +if Mode == 'Verify': + if os.path.isfile(Hash_Dir): + with open(Hash_Dir, 'r') as f: + if f.read() == M.hexdigest(): + print('Verification passed') + else: + print('Verification failed') +elif Mode == 'Delete': + for mod in Mods: + mod.delete() diff --git a/ModSlots/cmd.txt b/ModSlots/cmd.txt new file mode 100644 index 0000000..e276cbd --- /dev/null +++ b/ModSlots/cmd.txt @@ -0,0 +1 @@ +./ModSlots.py slots.txt /mnt/hdd/mods/mhw /mnt/ssd/SteamLibrary/steamapps/common/Monster\ Hunter\ World diff --git a/ModSlots/requirements.txt b/ModSlots/requirements.txt new file mode 100644 index 0000000..a93b1d1 --- /dev/null +++ b/ModSlots/requirements.txt @@ -0,0 +1,2 @@ +py7zr==1.0.0 +unrardll==0.1.7 |