python tools/mzinfo.py _work/hero-quest/QUEST.EXE
python tools/exepack.py _work/hero-quest/QUEST.EXE -o _work/QUEST.unp
| QUEST.EXE | INTRO.EXE | HERO.EXE | |
|---|---|---|---|
| file size | 51,209 | 23,017 | 1,075 |
| declared image size | 51,209 | 23,017 | 1,075 |
| overlay past the image | 0 | 0 | 0 |
| header paragraphs | 33 (528 B) | 32 (512 B) | 32 (512 B) |
| relocations | 0 | 0 | 0 |
entry point cs:ip |
0C2B:0012 | 055D:0012 | 000E:0012 |
| entry point file offset | 0xC4D2 | 0x57E2 | 0x2F2 |
initial stack ss:sp |
1498:0080 | 0837:0080 | 0029:0080 |
| min alloc | 6,076 para = 97,216 B | 12,287 para = 196,592 B | 13 para = 208 B |
| max alloc | 65,535 (all of it) | 65,535 | 65,535 |
| checksum | 0 | 0 | 0 |
| overlay number | 0 | 0 | 0 |
Three things fall out immediately and they all point the same way.
Every declared image size equals the file size exactly. No overlay, no appended data, no resource fork. Whatever these programs load, they load from the twenty-one other data files by name — and they do, as the string tables show below.
Every one of them declares zero relocations. A real DOS program compiled
for anything but the tiny model has relocations; zero means either a
.COM-shaped program wearing an MZ header or a packed image whose real
relocation table is hidden inside the payload.
All three entry points are at ip = 0x0012 and all three land within a few
hundred bytes of the end of the file. That is not a coincidence between three
independently built programs.
The signature scan finds RB two bytes before the entry point in all three
files, which is the classic Microsoft EXEPACK marker, and the entry bytes are
identical in all three:
8C C0 05 10 00 0E 1F A3 04 00 03 06 0C 00 8E C0 8B 0E 06 00 8B F9 4F 8B F7 FD F3 A4
mov ax,es / add ax,0010 / push cs / pop ds / mov [0004],ax / add ax,[000C] / ...
ip = 0x0012 is not arbitrary either: it is the length of the EXEPACK header,
which sits at cs:0000 and is followed by the decompressor stub. Reading that
header:
QUEST.EXE INTRO.EXE HERO.EXE
real entry cs:ip 0000:00ED 044B:0000 0000:0000
real stack ss:sp 2403:0190 3576:0064 000A:0032
exepack_size 841 537 339
dest_len (paragraphs) 5,219 2,069 19
skip_len 1 1 1
signature 'RB' 'RB' 'RB'
error message "Packed file is corrupt" in all three
Unpacking is a backwards walk: the source pointer descends from the end of the
packed region and the destination pointer descends from the top of the output
buffer, with three-byte commands (B0/B1 fill, B2/B3 copy, low bit ends
the stream). tools/exepack.py implements it, and it reports one thing worth
stating because it is where a first attempt goes wrong:
QUEST.EXE INTRO.EXE HERO.EXE
commands 165 74 5
FF padding stripped 14 10 2
verbatim head 12,273 2,722 151
src and dst meet True True True
unpacked size 83,504 33,104 304
expansion 1.676x 1.508x 1.369x
When the stream terminates, the source and destination pointers are equal in all three files. That is not luck: the stub does not move the whole image up before decompressing, so the low part of the load image is already sitting where it belongs and needs no work. Everything below the meeting point is copied across verbatim. A decoder that stops at the terminate flag and leaves that region zero produces a file that looks plausible and is 15–25 % missing, which is exactly what happened on the first run here.
The three decompressor stubs share their first 283 bytes exactly and diverge afterwards, where the per-file relocation table lives. One packer, one version, three files. That fact does most of the work in 03-the-1980-stamp.md.
| image | min alloc | resident (image + minalloc) | |
|---|---|---|---|
| QUEST.EXE | 83,504 | 97,216 | 180,720 |
| INTRO.EXE | 33,104 | 196,592 | 229,696 |
| HERO.EXE | 304 | 208 | 512 |
Plus a 256-byte PSP each. QUEST.EXE asks for 176 KB and INTRO.EXE for
224 KB before either has opened a file, and both then load pictures on top of
that — WIZBACK.VGA alone is 22 KB compressed. Both set max alloc to
0xFFFF, i.e. "give me all of conventional memory", and both carry the message
THERE IS INSUFFICIENT MEMORY FOR THE PROGRAM for when DOS cannot. The
INTRO.EXE figure being the larger of the two is the intro's decompression
buffers for the full-screen 320×200 pictures; the game itself never needs a
whole screen at once.
QUEST.EXE also carries REQUIRES DOS 2.0 OR GREATER..... — a version gate,
in a product shipping in 1992, seven years after DOS 3.0.
Unpacked, HERO.EXE is 304 bytes and contains three strings and nothing else:
000000E2 Insert disk 2 and press a key
00000101 $intro.exe
0000011A quest.exe
That is the whole program: prompt for the disk if needed, exec INTRO.EXE,
exec QUEST.EXE. 304 bytes cannot hold a C runtime, so it is hand-written
assembly, and its min alloc of 13 paragraphs confirms it — a C program's
startup code alone would ask for more. INSTALL.BAT ends by telling the player
Type HERO to play, and QUEST.EXE tells them Type 'hero' to set configuration when QUEST.CFG is missing, so HERO.EXE is both the launcher
and the setup entry point.
QUEST.EXE and INTRO.EXE contain a block that is byte-identical in both:
QUEST.unp 0xC9CA .. 0xCEEA
INTRO.unp 0x14AC .. 0x19CC
length 1,312 bytes (1.6 % of QUEST, 4.0 % of INTRO)
It holds, in order: the audio driver and bank filenames
(beeper.drv, adlib.drv, mt32.drv, tandy.drv, game&fx.alb,
game&fx.rld), the 87-glyph bitmap font (07-font-and-text.md),
the configuration menus, the error messages, and the keyboard scancode tables.
It is a linked library module and it is the reason both programs can draw text
and both can put up the same setup screen.
Its $-terminated strings are the giveaway that the setup screens are printed
with INT 21h/09h while everything else in the game is drawn with the bitmap
font:
Select screen format:-
1. MCGA/VGA (256 colours)
2. EGA
3. Tandy$
Select sound board:-
1. Internal
2. Adlib
3. Roland$
Centre joystick and press fire$
$quest.cfg
Heroquest (c) Gremlin Graphics 1991
$Configuration file missing,
Type 'hero' to set configuration
$No mouse installed
Three display modes, not two. MCGA/VGA, EGA and Tandy. The archive has
no .TDY files, so the Tandy path must render from one of the two existing
picture sets; and the sound menu has three entries — Internal, Adlib, Roland —
which does not mention Tandy at all, while TANDY.DRV exists and is the
largest of the four drivers. The most economical reading is that choosing
Tandy graphics also selects Tandy sound, which would explain why the sound menu
is one shorter than the driver count. That is an inference from two menus, not
a measurement, and it is listed as such in
12-open-questions.md.
QUEST.EXE's string table is a complete manifest of the game's data files, and
it is laid out as two parallel lists — fourteen .vga names then the same
fourteen .ega names, in the same order — which is how a program indexed by
display mode stores its filenames:
book monsters sprites odds&sod maps graphics furn shopsp shopmp bordered
wizback invent spellit men (each once as .vga, once as .ega)
questb.vga questb.ega equests.bin
english.bin french.bin spanish.bin german.bin italian.bin
quest1.bin .. quest24.bin
quest.sav quest.cfg
beeper.drv adlib.drv mt32.drv tandy.drv game&fx.alb game&fx.rld
shields.ega
INTRO.EXE's list is different and shorter, and it is the answer to why ten
pictures exist only in VGA form:
hero1.vga hero2.vga hero4.vga hero6.vga hero3.vga hero7.vga
gremlin.vga anims.vga pic1.vga text1.vga
shopmp.ega flags.ega bordered.ega
quest.cfg quest.exe intro.exe
Note the order: hero1, hero2, hero4, hero6, hero3, hero7. There is no
hero5.vga in the list, so the gap in the file numbering is a gap in the
numbering and not a missing file — the intro uses six pictures numbered
1, 2, 3, 4, 6, 7 and never asks for a fifth. That closes a question that the
directory listing alone could only raise.
Every file named by either executable exists in the archive, with the single
exception of quest.cfg, which is created at run time. There are no paths, no
drive letters and no development-machine directories anywhere in either
program; the strings are bare 8.3 filenames, all lower case, which is what a
program that does its own chdir looks like.
The copy-protection screen and the character-name entry screen live here, and
they are read in 05-install-and-copy-protection.md
and 07-font-and-text.md respectively. The epilogue —
five hundred bytes of prose about the Witch Lord, Skulmar and Sir Ragnar — is
in QUEST.EXE in plain uppercase English, not in any language file, which is
part of the localisation story in 06-language-files.md.