Archive Subsystem¶
Binary:
MW4Mercs.exe| Classified VAs: 285 | Exhaustive index: MW4Mercs.exe
#VBD archives, streams, file I/O, and path handling.
Role and boundary¶
The archive layer owns the game's #VBD container format, its directory and
hash chains, lazy member fetch/decompression, the stream interface, and the
file/path handling around archives. It is used by gameplay, render, UI, and the
database for content loading.
The archive is a directory plus member blobs:
- the header identifies the file as
#VBD(signature0x44425623) and stores the format version, maximum entry count, directory offset, and two keys; - the directory is a serialized list of entries, each with a file time, uncompressed size, stored size, data offset, entry index, and name;
- member data is stored either uncompressed or LZW-compressed, depending on whether compression was favorable.
Header and directory¶
The empty-archive initializer FUN_00413E90 sets:
| Offset | Meaning |
|---|---|
+0x00 |
header pointer |
+0x04 |
signature 0x44425623 (#VBD) |
+0x08 |
version (4) |
+0x0C |
maximum entries |
+0x10 |
file/directory base |
+0x14 |
key 1 |
+0x16 |
key 2 |
It also zeroes two hash tables at +0x18 and +0x100C; each is 1021 dwords
(0x3FD buckets). The bucket count is fixed.
The reader opener FUN_00413EE0 validates the signature and version (version
must be <= 4), reads the directory blob, initializes a stream over it, and
calls the directory populator FUN_00414110. If validation fails or the file
has fewer entries than requested, it creates an empty archive context instead.
The writer opener FUN_00414040 is the twin for a pre-initialized stream. It
reads the same header fields and calls the populator with absolute-offset
computation enabled; it does not allocate a directory buffer or perform file
I/O.
FUN_00414110 reads each directory entry, allocates an internal entry record,
populates it with FUN_00413480, and inserts it into both hash tables:
- table 1 at
+0x18, indexed byentry_index % 1021, chained throughentry->field_0C; - table 2 at
+0x100C, indexed byname_hash % 1021, chained throughentry->field_10.
The name hash FUN_00413500 is a modified ELF hash: it folds the low nibble of
each character, XOR-folds the high nibble into the low nibble when set, and
forces bit 31 in the result. The fold makes ASCII letters case-insensitive.
Name lookup compares the hash first, then uses _stricmp for collisions.
Lazy member fetch¶
FUN_00413830 is the member-fetch routine. For a file-backed archive:
- Seek to
header->field_10 + entry->data_off. - Allocate
entry->stored_sizebytes. - Read the stored bytes into the buffer.
- Store the buffer in
entry->field_08and setentry->field_26 = 1.
For an in-memory archive, it computes
entry->field_08 = header->field_10 + context->dir_file_offset + entry->data_off
instead.
If entry->stored_size < entry->uncompressed_size, the member is compressed:
the loader allocates an uncompressed_size buffer and calls the LZW
decompressor FUN_006FC220 to produce the final bytes. The raw reader
FUN_004138D0 performs the same seek/read but never decompresses; it is used
by the writer serializer.
The fetch dispatchers all share this loader:
FUN_00413BE0- find by entry index in table 1, then load if requested;FUN_00413C70- find by name in table 2, then load if requested;FUN_00413D20- iterator fetch; always loads when the guard conditions are met and has no load-flag parameter.
The iterator advance helpers FUN_00413D60/FUN_00413D80 load the current
entry and copy entry->field_08 into the iterator's data slot.
Writer path¶
FUN_00413540 serializes one entry. It calls the LZW compressor
FUN_006FBE80 when the compression ratio is favorable, then writes the entry
metadata and member bytes to the stream.
FUN_00414260 serializes the whole archive:
- Walk all 0x3FD buckets and sum the serialized size
(
name_len + 0x17per entry plusstored_size). - Build a stream with
FUN_0041EE70/FUN_0041E690. - Write the header fields through the stream's vtable write method.
- Iterate entries by index (
FUN_00413BE0) and serialize each withFUN_00413540. - Finalize with
FUN_004149C0and release the streams.
FUN_004143D0 is the writer variant. FUN_00414970/FUN_00414990/
FUN_004149A0 are flush-on-demand helpers.
Open, close, and naming¶
FUN_004145E0 is the primary file-backed opener. It stores the archive name
with refcounting (FUN_00412780), resolves the file path (including the CD
path from the registry), checks existence, and opens the file. FUN_00414820
is the stream-based opener: it stores the name, initializes the global
database object on first use, registers the archive name, and opens from the
existing stream.
FUN_004148F0 closes an archive:
- Free all directory entries and the header.
- Close the file handle, or the alternate stream object.
- Decrement the archive-name refcount.
- Decrement the global archive refcount; at zero, release the global database object.
- Release the name buffer when its refcount reaches zero.
Streams and path handling¶
The stream detail is the stream vtable used by the archive and database
layers: read/write, seek, advance/retreat, and bit-buffer variants. The
stream object is initialized by FUN_0041E690; read/write are FUN_0041E720/
FUN_0041E950; bit operations are FUN_0041EA60/FUN_0041EAE0 and
FUN_0041EB40/FUN_0041EC50.
Path handling lives in the archive opener and its helpers: archive name
storage, _mbsrchr filename extraction, CD-path lookup, and the path
normalization helpers around FUN_00412570/FUN_004125E0.
Invariants and failure modes¶
- The
#VBDsignature is0x44425623; a mismatched signature is not a valid archive. - The format version must be
<= 4; later versions are not accepted by the reader. - The directory hash tables have 1021 buckets; the writer and reader must agree on the bucket count and hash function.
- The name hash is case-insensitive by construction; lookup still resolves
collisions with
_stricmp. - A member is decompressed only when
stored_size < uncompressed_size; equal sizes mean the member is stored raw. entry->data_off == -1means the entry has no data and must not be loaded.- Lazy fetch caches the buffer in
entry->field_08and marks it withfield_26; a second fetch reuses the cached buffer. - The in-memory archive path computes offsets from the header base plus the directory offset; it does not perform file I/O.
- The writer's size pass and write pass must use the same bucket count and entry order, or the directory offsets are wrong.
Key entry points¶
| VA | Detail | What it does |
|---|---|---|
0x00413E90 |
vbd | empty-archive initializer |
0x00413EE0 |
vbd | reader opener |
0x00414040 |
vbd | writer opener |
0x00414110 |
vbd | directory populator |
0x00413480 |
vbd | per-entry directory parser |
0x00413500 |
vbd | entry-name hash |
0x00413830 |
vbd | lazy member fetch/decompress |
0x004138D0 |
vbd | raw member read |
0x00413BE0 |
vbd | fetch by index |
0x00413C70 |
vbd | fetch by name |
0x00413D20 |
vbd | iterator fetch |
0x00413D60/0x00413D80 |
vbd | iterator advance/load |
0x00413540 |
vbd | entry serializer |
0x00414260 |
vbd | archive writer |
0x004143D0 |
vbd | archive writer variant |
0x004145E0 |
vbd | file-backed archive open |
0x00414820 |
stream | archive open from stream |
0x004148F0 |
vbd | archive close |
0x00414A70 |
vbd | entry data hash/digest |
0x006FBE80 |
codec | LZW compressor |
0x006FC220 |
codec | LZW decompressor |
0x0041E690 |
stream | stream initializer |
0x0041E720 |
stream | stream read |
0x0041E950 |
stream | stream write |
Details¶
| Detail | VAs | Responsibility |
|---|---|---|
| vbd | 253 | #VBD archive directory, entry hashing, and member fetch. |
| stream | 30 | Stream vtable and memory/file stream implementations. |
| (general) | 2 | Archive helpers outside the #VBD/stream details. |
Coverage¶
All 285 classified VAs are in the MW4Mercs.exe index, which lists role, size, source, and evidence.