Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 offset means an offset in build/cleanroom/AGI.decrypted.exe.
  • image offset means file 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 offsetDOS functionObserved behavior
0x5cadAH=3cCreate/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.
0x5cceAH=3dOpen file. Takes path pointer in DX and mode in AL; returns 0xffff on carry/error.
0x5cefAH=3fRead file. Takes handle in BX, buffer in DS:DX, byte count in CX; returns zero on carry/error.
0x5d12AH=40Write file. Takes handle in BX, buffer in DS:DX, byte count in CX; returns zero on carry/error.
0x5d35AH=41Delete file. Takes path pointer in DX; returns zero on carry/error.
0x5d52AH=3eClose file handle in BX.
0x5d6bAH=42Seek. Takes handle in BX, offset in CX:DX, origin in AL; returns 0xffff:0xffff on carry/error.
0x5d94AH=45Duplicate file handle in BX; returns 0xffff on carry/error.
0x5db2AH=47Get 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.
0x5deaAH=19Return current drive as a lowercase letter by adding 0x61 to DOS’s zero-based drive number.
0x5e01AH=1a, then AH=4eSet 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.
0x5e26AH=4fFind-next using the current DTA; returns 0xffff on carry/error.
0x5e3eAH=19, AH=0e, AH=19, AH=0eProbe 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.
0x5e73AH=57, AL=0Get 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:

  1. Sets word [0x0615] = 1 and temporarily changes byte [0x0d15] to 0x40.
  2. Calls 0x85e5(0x73).
  3. If byte [0x0e72] is zero, formats and displays confirmation text rooted at 0x0db6, then waits through helper 0x4618.
  4. Creates file 0x1c8c through DOS wrapper 0x5cad.
  5. Writes 31 raw bytes from 0x1c6c.
  6. 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 pointerLength sourceLocal SQ2 lengthObserved role
0x00020x05e11505Main scalar/string/config/runtime block.
[0x096b][0x096f]903Object table bytes.
[0x0971][0x0975]328Three-byte entry table bytes.
[0x1707][0x0141] * 2200Resource-event pair buffer.
0x0985return value from 0x136416..28 observedFour-byte logic-number/resume-offset records, including a cache-head record and 0xffff terminator.

Restore action 0x7e (restore_game_state) at image offset 0x2512:

  1. Sets word [0x0615] = 1 and temporarily changes byte [0x0d15] to 0x40.
  2. Calls 0x85e5(0x72).
  3. If byte [0x0e72] is zero, formats and displays confirmation text rooted at 0x0d34, then waits through helper 0x4618.
  4. Opens file 0x1c8c through DOS wrapper 0x5cce.
  5. Seeks to offset 0x1f from the start of the file, skipping the raw description/header.
  6. 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 offsetMeaning
+0x04Destination segment.
+0x08Filename offset inside the overlay-table segment.
+0x0eMaximum 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:

NumberFileDestination segmentWord +0x06Name offsetParagraph countMax bytes
1CGA_GRAF.OVL0x09800x09ab0x00860x002b0x02b0
2JR_GRAF.OVL0x09800x099e0x00930x001e0x01e0
3EGA_GRAF.OVL0x09800x09a50x009f0x00250x0250
4HGC_GRAF.OVL0x09800x09db0x00ac0x005b0x05b0
5VG_GRAF.OVL0x09800x09970x00b90x00170x0170
6IBM_OBJS.OVL0x09db0x09f10x00c50x00160x0160
7HGC_OBJS.OVL0x09db0x0a010x00d20x00260x0260
8AGIDATA.OVL0x0a010x0bf10x00df0x01e80x1e80

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:

FlagStore
-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.