Decrypted AGI executable notes
This page records observations from build/cleanroom/AGI.decrypted.exe, the
MZ image reproduced from local SQ2 AGI bytes by tools/decrypt_agi.py with
an explicit --game-dir/AGI_GAME_DIR.
Address model
The decrypted file is a DOS MZ executable with header_paragraphs = 0x20.
Therefore file offset 0x0200 maps to runtime image offset 0x0000.
In this note:
file offsetmeans an offset inbuild/cleanroom/AGI.decrypted.exe.image offsetmeansfile offset - 0x0200.- Segment constants in the executable are MZ-relocated at load time. The values shown here are the pre-relocation values as stored in the decrypted image.
Startup overlay load
The MZ entry point is CS:IP = 0000:6777.
The entry routine at image offset 0x6777 performs a two-stage startup:
temporary_ss = 0x0be9 + (0x0080 >> 4)
temporary_sp = ((0x0be9 - 0x0a01) << 4) + 0x1000
SS:SP = temporary_ss:temporary_sp
save caller ES
DS = temporary_ss
ES = temporary_ss
load_overlay(8)
restore caller ES
SS:SP = 0x0be9:0x1000
DS = 0x0a01
[0x16af] = DS
[0x16b1] = caller ES
ES = DS
[0x16ab] = 0x0be9
jump 0x0078
The important point is that overlay number 8 is loaded before the program
switches DS to 0x0a01. Overlay 8 is AGIDATA.OVL, so the interpreter’s
main data segment is populated from that file before the main startup body at
image offset 0x0078 runs.
DOS file wrappers
Several small routines wrap DOS interrupt 21h file services:
| Image offset | DOS function | Observed behavior |
|---|---|---|
0x5cad | AH=3c | Create/truncate file. Takes path pointer in DX and attributes in CX; returns 0xffff on carry/error. The byte at 0x5cac is padding before the prologue. |
0x5cce | AH=3d | Open file. Takes path pointer in DX and mode in AL; returns 0xffff on carry/error. |
0x5cef | AH=3f | Read file. Takes handle in BX, buffer in DS:DX, byte count in CX; returns zero on carry/error. |
0x5d12 | AH=40 | Write file. Takes handle in BX, buffer in DS:DX, byte count in CX; returns zero on carry/error. |
0x5d35 | AH=41 | Delete file. Takes path pointer in DX; returns zero on carry/error. |
0x5d52 | AH=3e | Close file handle in BX. |
0x5d6b | AH=42 | Seek. Takes handle in BX, offset in CX:DX, origin in AL; returns 0xffff:0xffff on carry/error. |
0x5d94 | AH=45 | Duplicate file handle in BX; returns 0xffff on carry/error. |
0x5db2 | AH=47 | Get current directory for the default drive into caller buffer after first writing a leading backslash. If the returned path contains /, the helper writes / over the first byte, so slash normalization remains a little odd and should be rechecked dynamically. |
0x5dea | AH=19 | Return current drive as a lowercase letter by adding 0x61 to DOS’s zero-based drive number. |
0x5e01 | AH=1a, then AH=4e | Set the DTA pointer from the third argument, then perform find-first with pattern pointer in DX and attributes in CX; returns 0xffff on carry/error. |
0x5e26 | AH=4f | Find-next using the current DTA; returns 0xffff on carry/error. |
0x5e3e | AH=19, AH=0e, AH=19, AH=0e | Probe whether a lowercase drive letter can be selected. It saves the current drive, tries to set the requested drive, checks whether DOS reports that drive as current, restores the original drive, and returns 1 on success or 0 on failure. |
0x5e73 | AH=57, AL=0 | Get file date/time for handle BX; returns the time word in CX. |
Before most file-service calls, helper 0x5e8d temporarily switches DS to
segment 0x0a01 and clears word [0x184d]. The exact meaning of that global
is still open.
The helper at image offset 0x5b49 compares absolute buffer 0x0002 against
the embedded string SQ2\0 at image offset 0x5b6c, calling helper 0x02ae
on mismatch. It is reached by logic action handler 0x0e7e, which copies a
resolved logic message into 0x0002 before the check. In SQ2 this buffer is
both a runtime game signature and the save-file stem prefix used by the
filename formatter below.
The helper at image offset 0x5b73 formats savegame-like names with the local
format string at data offset 0x132c, %s%s%ssg.%d. Its arguments are the
path buffer at 0x1962, a separator string chosen from the current path
contents, the signature/prefix string at 0x0002, and the numbered slot.
Nearby local strings include slash/backslash separators at 0x1327, 0x1328,
and 0x135f, plus the user-facing example (For example, "B:" or "C:\savegame") at 0x1339. If 0x0002 contains SQ2, slot 1 formats as
SQ2SG.1; if that buffer is blank, the same helper formats SG.1.
The helper at image offset 0x5bdd validates or probes a user-supplied path:
it trims leading spaces, fills an empty path with the current directory helper
0x5db2, strips a trailing slash/backslash except for one-character paths,
records a drive-like letter in byte [0x1363], tests bare separators and
drive-only strings specially, and otherwise calls the find-first wrapper
0x5e01 with attribute mask 0x10. It returns 1 for accepted/probed paths and
0 for failure.
Save/restore state file
Logic actions 0x7d (save_game_state) and 0x7e (restore_game_state) are the observed save and restore paths. Both
use helper 0x85e5 to select or prepare a target slot/path and use the
savegame filename buffer at 0x1c8c.
Save action 0x7d (save_game_state) at image offset 0x2753:
- Sets word
[0x0615] = 1and temporarily changes byte[0x0d15]to0x40. - Calls
0x85e5(0x73). - If byte
[0x0e72]is zero, formats and displays confirmation text rooted at0x0db6, then waits through helper0x4618. - Creates file
0x1c8cthrough DOS wrapper0x5cad. - Writes 31 raw bytes from
0x1c6c. - Writes length-prefixed blocks through helper
0x28c6.
Helper 0x28c6(handle, pointer, length) writes:
u8 length_low
u8 length_high
length bytes from pointer
It returns 1 only when both one-byte length writes and the payload write return
the requested byte counts. The save action treats a short write of either the
31-byte header or any length-prefixed block as a recoverable save failure:
close the handle, delete the partial file named at 0x1c8c, display message
0x0e46, restore modal/text state, and continue after the opcode.
If file creation itself returns 0xffff, the save action displays formatted
message 0x0df0 using the path buffer at 0x1962, restores modal/text state,
and continues without attempting a delete.
The observed save blocks are:
| Source pointer | Length source | Local SQ2 length | Observed role |
|---|---|---|---|
0x0002 | 0x05e1 | 1505 | Main scalar/string/config/runtime block. |
[0x096b] | [0x096f] | 903 | Object table bytes. |
[0x0971] | [0x0975] | 328 | Three-byte entry table bytes. |
[0x1707] | [0x0141] * 2 | 200 | Resource-event pair buffer. |
0x0985 | return value from 0x1364 | 16..28 observed | Four-byte logic-number/resume-offset records, including a cache-head record and 0xffff terminator. |
Restore action 0x7e (restore_game_state) at image offset 0x2512:
- Sets word
[0x0615] = 1and temporarily changes byte[0x0d15]to0x40. - Calls
0x85e5(0x72). - If byte
[0x0e72]is zero, formats and displays confirmation text rooted at0x0d34, then waits through helper0x4618. - Opens file
0x1c8cthrough DOS wrapper0x5cce. - Seeks to offset
0x1ffrom the start of the file, skipping the raw description/header. - Reads length-prefixed blocks through helper
0x26b0.
Helper 0x26b0(handle, destination) reads a little-endian 16-bit length from
two one-byte reads, then reads that many bytes into destination, returning 1
only when all reads return the requested byte count.
If code.dos.open_file returns 0xffff, the restore action displays
formatted message 0x0d73, restores modal/text state, and continues after the
opcode. If opening succeeds but any length-prefixed block read fails, restore
closes the handle, displays message 0x0d87, and enters
code.system.exit_with_cleanup; this path terminates the DOS process rather
than returning to the current logic stream.
On successful restore, the handler refreshes display/resource state and writes
zero into its saved return pointer before leaving. The action therefore ends the
current logic stream instead of continuing after 0x7e; compatibility probes
must distinguish this from ordinary cancel/open-failure continuation.
The observed restore destinations mirror the saved blocks:
0x0002
[0x096b]
[0x0971]
[0x1707]
0x0985
The local SQ2 saves SQ2SG.1 through SQ2SG.11 all match
this envelope: a 31-byte description/header, four fixed-size blocks with
lengths 1505, 903, 328, and 200, and a fifth variable-size block. The
parser and serializer in tools/agi_save.py round-trip those saves
byte-for-byte and reject truncated blocks, trailing bytes, or inconsistent
block metadata.
Local strings in AGIDATA.OVL confirm the path roles: 0x0d34 starts
About to restore the game, 0x0d73 starts Can't open file, 0x0d87 starts
Error in restoring game, 0x0db6 starts About to save the game, 0x0df0
starts the directory-full/write-protected message, and 0x0e46 starts the
disk-full save-error message.
The shared selector helper 0x85e5 is the user-facing slot/path state machine.
It remembers whether the prompt marker was visible, erases that marker, saves
text attribute state, stops active sound, sets the text attribute pair, then
calls path prompt helper 0x8705. If no runtime path/description is already in
[0x0e72], 0x8705 displays the save or restore path question and edits the
path buffer at 0x1962 through modal edit helper 0x8794; path validator
0x5bdd accepts or rejects that path. Helper 0x86a3 displays the insert-disk
style message when the selected drive/path is unavailable.
Slot helper 0x8814 scans up to 12 numbered save files through summary reader
0x8b9f. Reader 0x8b9f formats a slot filename, opens it, reads the 31-byte
description into the slot summary record, seeks two bytes forward from the
current position to skip the first length prefix, reads seven bytes from the
first saved state block, and compares those bytes with the string at 0x0002.
This filters candidate saves to the current game signature/prefix. It also
records the DOS timestamp returned by 0x5e73. The selector displays the
available descriptions, marks the current row with a right-arrow glyph (0x1a),
clears the old row with a space, and loops on normalized events: Enter accepts,
Escape cancels, and movement events 1 and 5 move the current row up and down
with wrap. In save mode, accepting an empty-description slot opens a second
modal edit prompt rooted at 0x1baa and fills the 31-byte header buffer at
0x1c6c before the handler creates the file. On any exit, 0x85e5 restores
text state and redraws the prompt marker if it was visible before entry.
Overlay loader
The routine at image offset 0x67c3 loads one numbered overlay.
Pseudocode:
load_overlay(number):
saved_ds = DS
DS = relocated_segment(0x0970)
descriptor = 0x0004 + (number - 1) * 0x10
handle = dos_open(path = DS:[descriptor + 0x08], mode = 0)
saved_handle = handle
byte_count = DS:[descriptor + 0x0e] << 4
destination_segment = DS:[descriptor + 0x04]
BX = handle
CX = byte_count
DX = 0
DS = destination_segment
dos_read(BX, DS:DX, CX)
dos_close(saved_handle)
DS = saved_ds
There is no observed check of the read byte count in this routine. It asks DOS to read the descriptor-provided maximum byte count and then closes the file.
Overlay descriptor table
The overlay table is in the decrypted executable at file offset 0x9900, image
offset 0x9700. The first descriptor starts at image offset 0x9704. Each
descriptor is 16 bytes.
Observed fields used by load_overlay:
| Descriptor offset | Meaning |
|---|---|
+0x04 | Destination segment. |
+0x08 | Filename offset inside the overlay-table segment. |
+0x0e | Maximum read size in paragraphs; byte count is this value shifted left four bits. |
Other descriptor words are not yet fully explained. The word at +0x06
matches the destination segment plus the paragraph count for overlays 1-7. For
overlay 8 it is 0x0bf1, while destination plus paragraph count is 0x0be9;
this may be related to the temporary stack used during startup, but that remains
only a hypothesis.
Parsed descriptors:
| Number | File | Destination segment | Word +0x06 | Name offset | Paragraph count | Max bytes |
|---|---|---|---|---|---|---|
| 1 | CGA_GRAF.OVL | 0x0980 | 0x09ab | 0x0086 | 0x002b | 0x02b0 |
| 2 | JR_GRAF.OVL | 0x0980 | 0x099e | 0x0093 | 0x001e | 0x01e0 |
| 3 | EGA_GRAF.OVL | 0x0980 | 0x09a5 | 0x009f | 0x0025 | 0x0250 |
| 4 | HGC_GRAF.OVL | 0x0980 | 0x09db | 0x00ac | 0x005b | 0x05b0 |
| 5 | VG_GRAF.OVL | 0x0980 | 0x0997 | 0x00b9 | 0x0017 | 0x0170 |
| 6 | IBM_OBJS.OVL | 0x09db | 0x09f1 | 0x00c5 | 0x0016 | 0x0160 |
| 7 | HGC_OBJS.OVL | 0x09db | 0x0a01 | 0x00d2 | 0x0026 | 0x0260 |
| 8 | AGIDATA.OVL | 0x0a01 | 0x0bf1 | 0x00df | 0x01e8 | 0x1e80 |
The filename offsets are relative to the table segment. For example, descriptor
8 has name offset 0x00df, and the table segment begins at image offset
0x9700, so the string AGIDATA.OVL is at image offset 0x97df.
Command-line flags observed so far
The routine at image offset 0x00c4 reads the PSP command tail through the
segment saved at [0x16b1]. It scans for - followed by a single lower-case
letter and stores mode values in the interpreter data segment through ES.
Observed flags:
| Flag | Store |
|---|---|
-c | [0x1130] = 0 |
-r | [0x1130] = 1 |
-e | [0x1130] = 3 |
-h | [0x1130] = 2 |
-v | [0x1130] = 4 |
-p | [0x112e] = 0 |
-t | [0x112e] = 2 |
-s | [0x112e] = 8 |
The meanings of these variables are not fully proven yet. However, the overlay selection routine below uses these locations to choose graphics and object overlays.
Graphics and object overlay selection
The routine at image offset 0x821c chooses and loads two overlays using
load_overlay.
Pseudocode from observed control flow:
saved_value = call 0x5a74()
[0x1807] = saved_value
if [0x1130] == 2:
object_overlay = 7 # HGC_OBJS.OVL
graphics_overlay = 4 # HGC_GRAF.OVL
else:
object_overlay = 6 # IBM_OBJS.OVL
if [0x1130] == 3:
graphics_overlay = 3 # EGA_GRAF.OVL
else if [0x1130] == 4:
graphics_overlay = 5 # VG_GRAF.OVL
else if [0x112e] == 0:
graphics_overlay = 1 # CGA_GRAF.OVL
else:
graphics_overlay = 2 # JR_GRAF.OVL
load_overlay(graphics_overlay)
load_overlay(object_overlay)
call 0x5528()
This proves that AGIDATA.OVL is loaded during executable startup, while the
selected graphics and object overlays are loaded later by normal interpreter
initialization code.