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

Resource file notes

This page records clean-room observations about the SQ2 resource directory and volume files. It is based on local files from an explicitly selected SQ2 game directory (*DIR, VOL.*, AGIDATA.OVL) and the decrypted executable.

Directory file loading

The decrypted executable references these AGIDATA strings:

AGIDATA offsetString
0x116alogdir
0x1172viewdir
0x117apicdir
0x1182snddir

The routine at image offset 0x4305 loads all four directory files with the whole-file loader at image offset 0x3113.

Observed load order and destination pointers:

File stringPointer destination
logdir[0x11b2]
picdir[0x11b6]
viewdir[0x11b4]
snddir[0x11b8]

The whole-file loader at image offset 0x3113:

load_whole_file(path, optional_destination):
    handle = dos_open(path, mode=0)
    if open fails:
        display/retry error using AGIDATA string 0x0eef

    size = dos_seek(handle, offset=0, origin=end)
    dos_seek(handle, offset=0, origin=start)
    [0x0f1a] = size

    if optional_destination == 0:
        optional_destination = allocate(size)

    bytes_read = dos_read(handle, optional_destination, size)
    if bytes_read != size:
        display/retry disk error path

    dos_close(handle)
    return optional_destination

The exact retry behavior still needs refinement, but the open, seek-to-end, seek-to-start, optional allocation, full read, and close steps are directly visible in the disassembly.

Directory entry accessors

The four accessors multiply a resource number by three, add the result to the loaded directory base pointer, and reject entries whose first byte has a high nibble of 0xf.

Image offsetDirectory baseMissing-resource string
0x4371[0x11b2] (logdir)logic at AGIDATA offset 0x1140
0x43a5[0x11b4] (viewdir)view at AGIDATA offset 0x1146
0x43d9[0x11b6] (picdir)picture at AGIDATA offset 0x114b
0x440d[0x11b8] (snddir)sound at AGIDATA offset 0x1153

The shared check at image offset 0x434e:

entry_or_null(entry):
    if (entry[0] & 0xf0) == 0xf0:
        return 0
    return entry

So an absent resource directory entry is represented by a first byte whose high nibble is 0xf; in the SQ2 files observed so far this commonly appears as ff ff ff.

Directory entry format

Each loaded directory is an array of 3-byte entries.

The local SQ2 bytes support this interpretation:

byte0 high nibble: volume number
byte0 low nibble plus byte1 plus byte2: 20-bit offset in that VOL file

volume = byte0 >> 4
offset = ((byte0 & 0x0f) << 16) | (byte1 << 8) | byte2

Evidence from local samples:

DirectoryEntryRaw bytesDecoded targetBytes at target
LOGDIR010 6d 1bVOL.1 offset 0x06d1b12 34 01 b2
PICDIR110 96 8eVOL.1 offset 0x0968e12 34 01 a2
VIEWDIR000 9a 51VOL.0 offset 0x09a5112 34 00 1e
SNDDIR110 00 00VOL.1 offset 0x0000012 34 01 51

Many sampled entries across all four directories point to locations in VOL.* whose first two bytes are 12 34. The third byte in the sampled headers matches the decoded volume number.

Observed directory sizes:

FileSizeEntry count
LOGDIR426142
PICDIR444148
VIEWDIR720240
SNDDIR21070

Two entries at the ends of LOGDIR and PICDIR do not have an absent high nibble, but decode beyond the end of VOL.0:

DirectoryEntryRaw bytesDecoded target
LOGDIR14101 ff ffVOL.0 offset 0x1ffff
PICDIR14702 ff ffVOL.0 offset 0x2ffff

The current working interpretation is only that the executable’s generic directory-entry check would not reject these bytes. No observed local call path has attempted to load these resource numbers yet.

Volume file handles

The routine at image offset 0x3080 loops over volume numbers 0 through 4. For each number it formats AGIDATA string 0x0ee8 (vol.%d) into a local buffer, opens the resulting filename with the DOS open wrapper at 0x5cce, and stores the returned handle in a table starting at [0x0f04].

The routine at image offset 0x30d6 loops over the same five handle slots and closes every handle not equal to 0xffff, then resets the slot to 0xffff.

SQ2 contains VOL.0, VOL.1, VOL.2, and a tiny VOL.3; there is no VOL.4 in the current directory. The interpreter still has five handle slots.

Volume record header and generic reader

The generic resource reader starts at image offset 0x2e56. A retry wrapper at image offset 0x2e32 calls it until it returns a non-zero pointer or sets error state [0x0f02] to 5.

Given a non-null directory entry pointer and an optional destination pointer, the reader:

read_volume_resource(entry, optional_destination):
    if volume handles are not open:
        open VOL.0 through VOL.4

    volume = entry[0] >> 4
    handle = volume_handles[volume]
    offset = ((entry[0] & 0x0f) << 16) | (entry[1] << 8) | entry[2]

    seek(handle, offset, origin=start)
    header = read(handle, 5)

    require header[0] == 0x12
    require header[1] == 0x34
    require header[2] == volume

    payload_length = header[3] | (header[4] << 8)
    [0x0f1a] = payload_length

    if optional_destination == 0:
        optional_destination = allocate(payload_length)

    require read(handle, optional_destination, payload_length) == payload_length
    return optional_destination

The little-endian length is pinned down by the stack offsets in the disassembly: the 5-byte header is read to [bp-0x0f]..[bp-0x0b]; image offsets 0x2f73..0x2f85 compute ([bp-0x0b] << 8) + [bp-0x0c]. Since [bp-0x0c] is the fourth header byte and [bp-0x0b] is the fifth, this is header[3] | (header[4] << 8).

Local samples confirm the same layout. SNDDIR entry 1 points to VOL.1 offset 0x00000; the header bytes are 12 34 01 51 00, so the payload length is 0x0051. The next sound entry starts at 0x00056, exactly 5 header bytes plus 0x51 payload bytes later.

The resulting volume record layout is:

byte 0: 0x12
byte 1: 0x34
byte 2: volume number
byte 3: payload length low byte
byte 4: payload length high byte
byte 5..: payload bytes

Sampled validation over present entries:

DirectoryPresent entriesBad magicBad volume byteOut of range
LOGDIR119001
PICDIR75001
VIEWDIR203000
SNDDIR49000

The two out-of-range entries are the end entries shown in the directory-format section above.

Resource load call graph

Four higher-level loaders call the directory accessors and then the generic reader:

Resource typeLoaderExisting-cache lookupDirectory accessorGeneric reader call
Logic0x119a0x110f0x43710x11e0 -> 0x2e32
View0x39f70x39790x43a50x3a54 -> 0x2e32
Picture0x4a3b0x49e80x43d90x4a90 -> 0x2e32
Sound0x51260x50d80x440d0x5185 -> 0x2e32

The loaded resource payload returned by 0x2e32 does not include the 5-byte VOL.* record header; it begins at the resource-type-specific payload.

Gold Rush / AGI v3 resource container comparison

Gold Rush (games/GR) uses interpreter version string Version 3.002.149 in AGIDATA.OVL and changes the resource container without changing the decoded logic/picture/view/sound payload categories. Unlike SQ2, games/GR/AGI is already an MZ executable, so no SIERRA.COM decrypt step was needed before disassembling it.

The v3 game directory is a single combined GRDIR file instead of four split directory files. Its first eight bytes are four little-endian section offsets:

Header wordSectionOffsetEntry countPresent entries
+0logic0x0008245182
+2picture0x02e7245186
+4view0x05c6256247
+6sound0x08c65144

The section payloads are still 3-byte directory entries using the same packed volume/offset calculation as SQ2. The absent-entry test changed, though: SQ2’s shared check rejects any entry whose first byte has high nibble 0xf, while the v3 shared check at GR image 0x4599 rejects only the exact byte sequence ff ff ff.

GR image 0x44de is the v3 directory loader corresponding to SQ2 code.resource.load_all_directories. It formats a combined directory filename from the runtime prefix and the "%sdir" string in AGIDATA.OVL. When the combined file opens, the loader reads it whole and stores section-base pointers derived from the first four header words. If the combined open fails, the same routine falls back to separate logdir, picdir, viewdir, and snddir loads.

The v3 volume files are named with a prefix, for example GRVOL.0 through GRVOL.12. GR image 0x33c2 opens sixteen possible volume handles by formatting "%svol.%d", so higher volume numbers are part of the observed v3 container design rather than malformed directory entries.

The v3 generic reader starts at GR image 0x30d0, with a retry wrapper at 0x30ac. It reads a 7-byte record header:

byte 0: 0x12
byte 1: 0x34
byte 2: metadata/volume byte
byte 3: expanded length low byte
byte 4: expanded length high byte
byte 5: stored length low byte
byte 6: stored length high byte
byte 7..: stored bytes

If metadata bit 0x80 is clear, the metadata byte must equal the directory entry volume. If metadata bit 0x80 is set, the reader records a picture-specific transform flag, masks the metadata byte to its low nibble, and then compares that low nibble with the directory entry volume.

After allocation, v3 has three payload paths:

ConditionTransformGR image offsetObserved resource families
metadata bit 0x80 setpicture nibble expansion0x9a5bpictures
expanded length equals stored lengthdirect read0x607d DOS read wrappera few logic/sound resources
expanded length differs from stored lengthdictionary expansion0x07f4most logic, all views, most sounds

The dictionary path is a bitstream decoder with 9-bit initial codes, growth to 10 and 11 bits, reset code 0x100, and end code 0x101. Dictionary entries are prefix/suffix pairs; after each ordinary code the decoder adds previous_string + first_byte(current_string) at the next dictionary slot.

The picture path is not the same dictionary compression. It is a nibble realigner: when the stream emits picture command 0xf0 or 0xf2, the next color/control operand is packed into one nibble. The transform expands those nibbles back into ordinary byte-sized picture command operands and stops after emitting 0xff.

tools/agi_resources.py implements the observed split v2 and combined v3 container formats. Local tests in tests/test_agi_resources.py currently validate:

  • SQ2 detection as split-directory/direct-record layout.
  • GR detection as combined-directory/7-byte-record layout.
  • Synthetic dictionary reset/literal/end expansion.
  • Synthetic 0xf0/0xf2 picture-nibble expansion.
  • Synthetic picture-nibble encoding back to the observed packed storage form.
  • v3 copied-fixture patching for direct logic/view records and picture-nibble picture records.
  • All present GR resources expanding to their header-declared lengths.

The full GR transform census from the local parser is:

Resource familyDirectDictionaryPicture nibble
logic21800
picture00186
view02470
sound3410

tools/qemu_fixture.py can now write generated v3 fixtures without modifying games/. It appends direct records for generated logic/view payloads and picture-nibble records for generated picture payloads, then patches the selected entry in the combined directory. This is intentionally not a general v3 dictionary compressor: original compressed local resources are read through the decoder, while controlled synthetic fixtures use the direct-record path already present in the observed interpreter.

The generated v3 fixture path is also original-engine checked. QEMU report build/gr-v3-behavior/synthetic_picture_view_suite.json compares a blank control, a generated picture-nibble picture fixture, and a generated picture-plus-direct-view fixture. The picture-only capture differs from blank by 215,040 pixels, and the picture-plus-view capture differs from picture-only by 128 pixels, proving that the original GR interpreter accepts both generated v3 record forms for this controlled case.

The logic disassembler now uses tools/agi_resources.py for payload loading and detects AGIDATA dispatch-table bases from the observed operand metadata signature. Observed table bases are:

Local inputAction tableCondition table
SQ2 / v20x061d0x08fd
GR / v30x04400x0762
KQ4D / v30x06200x0942

The GR and KQ4D v3 action dispatchers support action opcodes through 0xb5. The decoded Gold Rush scripts observed so far only use actions through 0xa9. The decoded KQ4D demo scripts use the normal sound actions 0x62..0x64 and only reference sound resources 70..79, which are the clean v3 sound records in that local directory. Later present-looking KQ4D sound-section triples that do not point at 12 34 headers are therefore planning evidence for source inspection, not currently observed valid-resource behavior.

The GR condition dispatcher compares predicate bytes with 0x26, but only entries 0x00..0x12 are structured condition-table records in the observed AGIDATA.OVL; bytes above that overlap string/data and are not treated as confirmed predicates. Local GR scripts observed so far use condition opcodes only through 0x0e.

Resource cache records

The four resource families keep separate singly linked cache lists. In every observed record, word +0x00 is the next-record pointer. The lookup helpers walk the list for a matching resource number and also leave an insertion/link slot in a family-specific global so the loader can append a miss without rescanning.

Resource typeRoot and insertion stateLookupRecord sizeRecord fields
Logicroot [0x0977], previous-link slot [0x0983]0x110f10 bytes+0x00 next, +0x02 logic number byte, +0x03 message count byte, +0x04 bytecode pointer (payload + 2), +0x06 current instruction pointer, +0x08 message offset table pointer
Viewroot [0x0ffa], previous-link slot [0x1000]0x39795 bytes+0x00 next, +0x02 view number byte, +0x03 payload pointer
Picturestatic first record/root [0x120e], previous-link slot [0x1214]0x49e85 bytes+0x00 next, +0x02 picture number byte, +0x03 payload pointer
Soundstatic first record/root [0x125a], previous-link slot [0x1268]0x50d814 bytes+0x00 next, +0x02 sound number word, +0x04 payload pointer, +0x06/+0x08/+0x0a/+0x0c channel stream pointers

View loader 0x39f7 allocates a 5-byte cache record on a miss and writes it through the link slot left in [0x1000]. Picture loader 0x4a3b and sound loader 0x5126 use their static first records when the previous-link slot is zero, then allocate later records (5 bytes for pictures and 14 bytes for sounds). Sound loader 0x5126 derives the four channel stream pointers by reading the first four little-endian words in the payload and adding each offset to the payload base.

The same saved link slot gives picture/view discard its stack-shaped effect. View discard helper 0x3f0d finds the selected record through 0x3979, then stores zero through [0x1000]. Picture discard 0x4bce does the equivalent through [0x1214]. The cleared word is the family root when the match is first, or the preceding record’s next link otherwise. Consequently the selected record and every later record in that family become unreachable together; this is not arbitrary single-entry deletion.

Both helpers flush the object update lists, pass the selected record address to code.heap.rewind_to, rebuild the lists, and refresh the free-memory state. Because the allocation frontier is shared by resource families, valid scripts must not keep using a selected payload after discarding its record. The clean spec expresses the stable family-visible truncation rule and excludes stale payload use rather than exposing allocator addresses.

Selected local scripts follow the expected stack discipline. KQ2 logic 67 loads views 53,59,51,52,57,60 and discards them as 60,57,52,51,59,53. Common picture setup paths load and prepare a picture, then discard it before loading the room’s views and sounds. This is game-data support for the source-derived list/rewind rule, not the basis for it.

Early payload observations from these loaders:

  • Logic payloads begin with a little-endian offset. Loader 0x119a treats payload + 2 as an instruction/data pointer, reads the first two payload bytes as a little-endian offset, then reads one byte at payload + 2 + offset into the logic cache record.
  • Sound loader 0x5126 reads the first four little-endian words in the sound payload as offsets relative to the payload base and stores four derived pointers in its cache record.
  • View and picture loaders store the returned payload pointer directly in their cache records. Later graphics/object routines parse view group/frame tables and picture command streams as described in Graphics and Object Pipeline.