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 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-18 11:55:10 -04:00
1 parent 8a8ebc5632
commit b2e38bdc06
2 files changed
+85 -47

No files matched your search

+17 -11
View File
@@ -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):
+68 -36
View File
@@ -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/<name> and packs back to <name>.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)