From b2e38bdc06b1f744a39c3a676d06bc10a88bc0ec Mon Sep 17 00:00:00 2001 From: MagicBot Date: Sun, 18 Oct 2026 11:55:10 -0400 Subject: [PATCH] Support all .wpak files with a wpak target Replaces the config target with wpak, which handles every known pak or a single one named on the command line. Map icon .tga entries are stored unencrypted (sb.exe 0xAACA60). Co-Authored-By: Claude Opus 5.5 --- mbEditorPro2.0/arcane/util/ArcWpak.py | 28 ++++--- mbEditorPro2.0/mbEditorPro.py | 104 +++++++++++++++++--------- 2 files changed, 85 insertions(+), 47 deletions(-) diff --git a/mbEditorPro2.0/arcane/util/ArcWpak.py b/mbEditorPro2.0/arcane/util/ArcWpak.py index 5b06b64..1efb8b6 100644 --- a/mbEditorPro2.0/arcane/util/ArcWpak.py +++ b/mbEditorPro2.0/arcane/util/ArcWpak.py @@ -13,7 +13,8 @@ import zipfile from .ArcFileCache import next_available_path # A .wpak is a zip (client uses minizip, ArcWPakFile 0x54D3C0) whose entries are -# each Blowfish CFB-64 encrypted with a zero IV (BF_cfb64_encrypt 0xCD9000). +# each Blowfish CFB-64 encrypted with a zero IV (BF_cfb64_encrypt 0xCD9000), +# except image entries. WPAK_KEY = bytes([ 0x85, 0x71, 0x40, 0x3C, 0x14, 0x50, 0x0B, 0x52, 0x73, 0x2D, 0x10, 0x08, 0x63, 0x59, 0x5B, 0xAA, @@ -21,6 +22,9 @@ WPAK_KEY = bytes([ WPAK_COMPRESS_LEVEL = 9 # zipOpenNewFileInZip level in ArcWPakFile writer +# Map icon paks keep their .tga entries unencrypted (LoadMapZoneData 0xAACA60). +WPAK_PLAIN_EXTENSIONS = ('.tga',) + MASK32 = 0xFFFFFFFF @@ -129,6 +133,10 @@ class ArcBlowfish: WPAK_CIPHER = ArcBlowfish(WPAK_KEY) +def is_encrypted_entry(filename): + return not filename.lower().endswith(WPAK_PLAIN_EXTENSIONS) + + def load_wpak_file(filename): entries = [] @@ -136,10 +144,10 @@ def load_wpak_file(filename): for info in wpak_file.infolist(): if info.is_dir(): continue - entries.append([ - info.filename, - WPAK_CIPHER.cfb64_decrypt(wpak_file.read(info)) - ]) + data = wpak_file.read(info) + if is_encrypted_entry(info.filename): + data = WPAK_CIPHER.cfb64_decrypt(data) + entries.append([info.filename, data]) return entries @@ -147,7 +155,7 @@ def load_wpak_file(filename): def load_wpak_directory(directory): entries = [] - # Original archives list entries in NTFS directory order (uppercase-folded sort). + # Client finds entries by name (unzLocateFile 0xCD7920), so order only needs to be stable. for filename in sorted(os.listdir(directory), key=str.upper): filepath = os.path.join(directory, filename) if not os.path.isfile(filepath): @@ -163,11 +171,9 @@ def build_wpak_bytes(entries, wpak_file): for filename, data in entries: info = zipfile.ZipInfo(filename, date_time=time.localtime()[:6]) info.compress_type = zipfile.ZIP_DEFLATED - archive.writestr( - info, - WPAK_CIPHER.cfb64_encrypt(data), - compresslevel=WPAK_COMPRESS_LEVEL - ) + if is_encrypted_entry(filename): + data = WPAK_CIPHER.cfb64_encrypt(data) + archive.writestr(info, data, compresslevel=WPAK_COMPRESS_LEVEL) def save_wpak_file(filename, entries): diff --git a/mbEditorPro2.0/mbEditorPro.py b/mbEditorPro2.0/mbEditorPro.py index dcc0aee..dd685e2 100644 --- a/mbEditorPro2.0/mbEditorPro.py +++ b/mbEditorPro2.0/mbEditorPro.py @@ -45,7 +45,6 @@ MOTION_DIRECTORY = os.path.join(TARGET_DIRECTORY, 'MOTION') TILE_DIRECTORY = os.path.join(TARGET_DIRECTORY, 'TILE') SKELETON_DIRECTORY = os.path.join(TARGET_DIRECTORY, 'SKELETON') RENDER_DIRECTORY = os.path.join(TARGET_DIRECTORY, 'RENDER') -CONFIG_DIRECTORY = os.path.join(TARGET_DIRECTORY, 'Config') COBJECTS_MAGIC = 0x434c4e54 COBJECTS_MAP = { @@ -86,10 +85,25 @@ EDITOR_TARGETS = [ 'skeleton', 'terrain', 'render', - 'config', + 'wpak', 'all' ] +# Each pak unpacks to ARCANE_DUMP/ and packs back to .wpak. +WPAK_NAMES = [ + 'AerynthIcons', + 'Config', + 'DalgothIcons', + 'Environment', + 'GeneralItemTables', + 'ItemTables', + 'ModTables', + 'ModTypeTables', + 'Transitions', + 'VendorENGLISH', + 'VorringiaIcons' +] + def create_unpack_directory(base_directory): # Never unpack over an existing folder; use the next free numbered name. @@ -155,11 +169,6 @@ def init_render(): os.makedirs(directory_path, exist_ok=True) -def init_config(): - directory_path = CONFIG_DIRECTORY - os.makedirs(directory_path, exist_ok=True) - - def unpack_cobjects(): resources = load_cache_file('CObjects.cache') directory = create_unpack_directory(COBJECTS_DIRECTORY) @@ -848,37 +857,59 @@ def test_render(): print(res_id, in_stream.buffer.tell(), out_stream.buffer.tell()) -def unpack_config(): - entries = load_wpak_file('Config.wpak') - directory = create_unpack_directory(CONFIG_DIRECTORY) +def selected_wpak_names(): + # An optional third argument picks a single pak, e.g. "unpack wpak Config". + return sys.argv[3:] or WPAK_NAMES + + +def unpack_wpak(): + for wpak_name in selected_wpak_names(): + wpak_filename = wpak_name + '.wpak' + if not os.path.isfile(wpak_filename): + print('***Skipping: ' + wpak_filename + ' not found.') + continue + + entries = load_wpak_file(wpak_filename) + directory = create_unpack_directory(os.path.join(TARGET_DIRECTORY, wpak_name)) + + with tqdm(total=len(entries)) as pBar: + for filename, data in entries: + filepath = os.path.join(directory, filename) + with open(filepath, 'wb') as fp: + fp.write(data) + pBar.update(1) + + +def pack_wpak(): + for wpak_name in selected_wpak_names(): + directory = os.path.join(TARGET_DIRECTORY, wpak_name) + if not os.path.isdir(directory): + print('***Skipping: ' + directory + ' not found.') + continue + + entries = load_wpak_directory(directory) + save_wpak_file(wpak_name + '.wpak', entries) + + +def test_wpak(): + for wpak_name in selected_wpak_names(): + wpak_filename = wpak_name + '.wpak' + if not os.path.isfile(wpak_filename): + print('***Skipping: ' + wpak_filename + ' not found.') + continue + + print(wpak_filename) + entries = load_wpak_file(wpak_filename) + out_stream = io.BytesIO() + + build_wpak_bytes(entries, out_stream) + repacked = dict(load_wpak_file(out_stream)) - with tqdm(total=len(entries)) as pBar: for filename, data in entries: - filepath = os.path.join(directory, filename) - with open(filepath, 'wb') as fp: - fp.write(data) - pBar.update(1) - - -def pack_config(): - init_config() - - entries = load_wpak_directory(CONFIG_DIRECTORY) - save_wpak_file('Config.wpak', entries) - - -def test_config(): - entries = load_wpak_file('Config.wpak') - out_stream = io.BytesIO() - - build_wpak_bytes(entries, out_stream) - repacked = dict(load_wpak_file(out_stream)) - - for filename, data in entries: - try: - assert repacked[filename] == data - except: - print(filename, len(data), len(repacked.get(filename, b''))) + try: + assert repacked[filename] == data + except: + print(filename, len(data), len(repacked.get(filename, b''))) def unpack_all(): @@ -922,6 +953,7 @@ def usage(): print('') print('example: unpack texture') print('example: pack texture') + print('example: unpack wpak Config') exit(1)