Plain Python 3, no dependencies. Nothing here needs a disassembler, an image
library or a hex editor. Every tool writes to stdout and the saved output of
every run is in notes/, so every figure in the documents can be
checked without having the game.
Run order, which is also the order the session went in:
python tools/ziplist.py HeroQuest_DOS_EN.zip > notes/ziplist.txt
python tools/census.py _work/hero-quest > notes/census.txt
python tools/mzinfo.py _work/hero-quest/QUEST.EXE > notes/mz.txt
python tools/exepack.py _work/hero-quest/QUEST.EXE -o _work/QUEST.unp
python tools/strdump.py _work/QUEST.unp --min 5 > notes/str-exe.txt
python tools/font.py _work/QUEST.unp > notes/font.txt
python tools/lang.py _work/hero-quest --strict --run 6 > notes/lang.txt
python tools/quest.py _work/hero-quest > notes/quest.txt
python tools/gfx.py _work/hero-quest --codec > notes/gfx.txt
python tools/audio.py _work/hero-quest > notes/audio.txt
python tools/sav.py _work/hero-quest/QUEST.SAV > notes/sav.txt
python tools/licence.py _work/hero-quest _work > notes/licence.txt
ziplist.py — the ZIP central directory, with each entry's MS-DOS date and
time re-encoded back into the raw 16-bit words so the timestamp can be read as
the filesystem stored it rather than as a library interpreted it. Also compares
the local file header against the central directory for every entry, which
is the check that eliminates the container from
03-the-1980-stamp.md in one pass. Produces the date
histogram, the year histogram and the extension census.
census.py — per-file size, SHA-1, whole-file Shannon entropy, zero
fraction, printable fraction, last non-zero offset, and the first sixteen bytes
as hex and text. Also checks for byte-identical files. The first-sixteen-bytes
column is what first showed that ADLIB.DRV and MT32.DRV share a prologue,
that PIC1.VGA and TEXT1.VGA share a header, and that QUEST*.BIN begins
with a run of small ascending integers.
mzinfo.py — MZ header arithmetic (declared image size against file size,
which is how an overlay is found), the relocation table, entry-point bytes, a
17-signature packer scan, a 4 KiB-window entropy profile and the longest zero
run. The packer scan is reported as "N signatures, M hits" because a scan that
finds nothing proves nothing; the header arithmetic and the entry-point bytes
are the real evidence and they are printed next to it.
exepack.py — identifies and undoes Microsoft EXEPACK. Reads the 18-byte
header at cs:0000, checks the RB signature and the 8C C0 05 10 00 stub
head, strips the 0xFF padding, and runs the backwards decompression. Reports
src_dst_meet, which is the check that catches the one mistake this format
invites — see below.
strdump.py — a strings replacement, because this shell has none. Runs of
printable bytes with file offsets; --high accepts 0x80–0xFF and reports a
census of which high bytes occurred; --dollar filters to $-terminated runs,
the DOS INT 21h/09h convention.
font.py — finds the bitmap font by searching for the glyph of 0 (a
slashed zero, a very recognisable bit pattern), derives the table base from the
character code, finds the end by the first 40-byte zero run, and renders every
glyph as ASCII art. Also checks each glyph in 0x60–0x76 for byte-identity
against 0x20–0x5F, which is what revealed that seven of them are copies of
G–M.
lang.py — the five language files: table geometry at both byte alignments,
a six-way byte-class census, and the accent census. --strict counts a byte as
a literal only when it sits inside a run of at least N drawable bytes, which is
necessary because the second byte of a two-byte reference is otherwise
indistinguishable from a character. Also prints the longest uncompressed runs
per language, which is where GESCHaFTIG and L'AVVERSARIO e MORTO came from.
quest.py — diffs the twenty-four quest files against each other before
parsing any one of them: distinct grids, the per-offset distinct-value profile,
the constant regions, the tails, and the printable-run census. It prints the
26 × 19 grid because the grid is the finding.
gfx.py — the per-pair EGA/VGA size and entropy table, the naive ratio and
the paired ratio side by side, the EGA-only and VGA-only groups, and with
--codec a partial decode of every picture file reporting where it stops, plus
a raw opcode-range census.
audio.py — the four drivers' entry bytes, zero fractions and pairwise
common prefixes (a test for a shared runtime); the banks' module name, order
list and 16-byte directory records; and the instrument names after the
directory.
sav.py — QUEST.SAV's 32 × 17-byte name slots and 32 × 26-byte records.
licence.py — the same scan run against the CD32 sequel, over every file in
this installation, for the names that would have to appear if the licence were
written rather than painted. Reports single-byte copyright-sign candidates
separately and labels them as noise, because they occur by chance thousands of
times in compressed data.
Zero attributable strings in all four audio drivers. No author, no company,
no version, no date, in 22,092 bytes. The same scan over INTRO.EXE returns
HASBRO, GAMES WORKSHOP, BRADLEY, GREMLIN, IMAGITEC, WOMACK,
GLOSSOP, HARLING in one pass, so the tool works and the drivers are silent.
Zero occurrences of any licence term in any of the forty-two picture files.
HASBRO, GAMES WORKSHOP, BRADLEY, MILTON, GREMLIN, IMAGITEC — none of
them, anywhere, in 585,807 bytes of graphics. That is the measurement that says
the notice is not painted, and it is the direct counterpart of the CD32
sequel's result where the notice is only painted.
Zero byte-identical files. All 84 SHA-1s are distinct.
Zero overlays. All three executables declare an image size exactly equal to their file size.
Zero relocations declared in all three MZ headers — which is not "nothing to see", it is the tell that identified EXEPACK.
Zero paths, drive letters or development directories in any executable
string. Every filename in the archive is a bare lower-case 8.3 name. Some
executables of this era are full of C:\WORK\...; this one is not, and the only
thing that points at a physical machine is a keyboard table
(12-open-questions.md, question 9).
Zero files stamped 1980 other than QUEST.EXE, and zero files stamped after
1992 other than QUEST.SAV. Both were checked before either was theorised
about.
588 candidate bit layouts for the language-file back-reference model, none of which decoded past eleven bytes, and seven candidate absolute-offset arithmetics, none of which lands on the required expansion. Recorded in 12-open-questions.md so that nobody repeats it.
exepack.py prints src_dst_meet for a reason. EXEPACK's stub does not relocate
the whole load image before decompressing; the low part of the image is already
in place. When the command stream hits its terminate flag, the source and
destination pointers are equal, and everything below that point must be
copied across verbatim from the packed file. A decoder that stops at the
terminate flag and leaves the rest zero produces an output of exactly the right
declared length, containing plausible code in its upper region, and missing
15–25 % of the program:
QUEST.EXE INTRO.EXE HERO.EXE
verbatim head 12,273 2,722 151
of unpacked size 83,504 33,104 304
missing if ignored 14.7 % 8.2 % 49.7 %
The first run of this session produced exactly that and it looked fine.
src_dst_meet being True in all three files is what proves the copy is
correct rather than a patch over a bug.
The extracted game (_work/) and everything derived from it that is still the
game — _work/QUEST.unp, _work/INTRO.unp, _work/HERO.unp — are in
.gitignore along with *.EXE *.VGA *.EGA *.BIN *.DRV *.SAV *.zip *.png. No
asset, no picture and no executable is in this repository, packed or unpacked.
The text extracts quoted in the documents are quotations, and the saved tool
output in notes/ is measurement.