MacVenture File Formats

The following is documentation of the various file formats used by MacVenture games. I have reverse engineered these formats myself, so they're probably not 100% accurate.
All types are big-endian unless otherwise noted

Table of Contents

Overview

It is assumed you can read HFS disk images. It is also assumed you can parse a file's resource fork. All of this is documented already elsewhere. However, I will describe the individual resource formats because although they're documented in places, it is difficult to find documentation specific to the old monochrome resources, and that documentation is often incomplete.

The main executable file contains a resource fork which defines the windows, menu items, dialog boxes, controls, text compression, string tables, and filenames of the game data files.

The main executable file can be identified by the file type 'APPL' and file creator 'MCV#' where # is the game number.

  1. Deja Vu
  2. Uninvited
  3. Shadowgate
  4. Deja Vu II

From here, you can branch off and locate the other data files needed by the game. These files include:

Some of these files have misleading names. For example, Uninvited's Diploma file is called "Uninvited Math", presumably to keep out casual snoopers.

Main Resource Fork

The main executable resource fork contains several resources, the ones we care about are:

ALRT
Alert boxes:
$83
Shown when you quit without saving your current game.
$86
Shown when you lose, prompts to restart quit or load.
CNTL
Controls.
$80
Exit box in Exits window.
$81 and above
Commands in the Comamnd window.
DITL
Dialog item lists. Defines the controls used in dialog boxes.
DLOG
Dialog boxes:
$84
Shown when you speak, prompts for text.
$87
Shown when you win, prompts for your name for your diploma.
ICN#
Icons:
$80
Game icon
$81
Save game icon
GNRL
General resources:
$80
General settings. See below.
$81
Diploma geometry. See below.
$83
Text Huffman table. See text decoding.
MENU
Menu definitions.
STR
String definitions:
$82
Save game prompt
$83
Diploma filename
$84
Click to continue text
$85
Start game filename
STR#
String tables:
$80
Error strings, and Untitled filename
$81
Filenames to game data files:
  1. MCV ID
  2. Title filename
  3. Subdirectory filename
  4. Object filename
  5. Filter filename
  6. Text filename
  7. Graphic filename
  8. Sound filename
$82
Common articles for text decoding (the, if, at..)
$83
Articles for item naming (the, a, an..)
$84
Indirect articles for item naming (he, she, it..)
WIND
Windows:
$80
Commands window
$81
Main game window
$82
Text window
$83
Self window
$84
Exits window
$85
Diploma window

ALRT format

The ALRT resource describes modal alert boxes.

struct ALRT {
    uint16_t top; //bounds of the alert box
    uint16_t left;
    uint16_t bottom;
    uint16_t right;
    uint16_t ditl; // ID of the DITL resource which contains the
                   // controls for this alert box.
};

CNTL format

The CNTL resource describes control widgets.

struct CNTL {
    uint16_t top;       //bounds of control
    uint16_t left;
    uint16_t bottom;
    uint16_t right;
    uint16_t value;     //scrollbar value
    uint8_t  visible;   //0 = not visible
    uint8_t  unused;
    uint16_t max_value; //scrollbar max
    uint16_t min_value; //scrollbar min
    uint16_t cdef;      //control type, see below
    uint32_t refcon;    //reference constant
    uint8_t  title_length;
    char[]   title;     //title for button
};
#define CDEF_button    0x0000
#define CDEF_checkbox  0x0001
#define CDEF_radio     0x0002
#define CDEF_scrollbar 0x0010
#define CDEF_custom    0x0800 // square button, used for exits and commands

DITL format

struct DITL {
    uint16_t numItems; // num items minus 1
    DITLItem items[];
};
struct DITLItem {
    uint32_t reserved;
    uint16_t top;     // bounds of control
    uint16_t left;
    uint16_t bottom;
    uint16_t right;
    uint8_t  type;    // type of control, see below (only 7 bits used).
    uint8_t  title_length;
    char title[];     // title of control
    uint8_t padding;  // optional, only present if title_length is odd.
};
#define DITL_button   0x04
#define DITL_label    0x08
#define DITL_textedit 0x10
The control's refcon should be set to its position in the list, the first control has a refcon of 1, the second has a refcon of 2, etc.

DLOG format

struct DLOG {
    uint16_t top;        // bounds for dialog box
    uint16_t left;
    uint16_t bottom;
    uint16_t right;
    uint16_t type;       // type of dialog, see WIND
    uint8_t  visibility; // 0 = invisible
    uint8_t  unused;
    uint8_t  closeBox;   // 0 = no close box
    uint8_t  unused;
    uint32_t refcon;     // Reference Constant
    uint16_t ditl;       // ID of the DITL resource for this dialog
    uint8_t  title_length;
    char     title[];    // title of dialog box
};

ICN# format

The Icon format is straight-forward. Each icon is 32x32, 1 bpp. The first 128 bytes of the icon are the icon image data, 4 bytes per row, 1 bit per pixel. The next 128 bytes of the icon are the icon mask, off bits are transparent.

GNRL formats

Each GNRL resource has its own format.

General settings $80

The general settings table is used to define game settings like how many objects are defined in the game, object attribute masks, which commands have buttons etc.
struct GeneralSettings {
    uint16_t numObjects;    // number of game objects defined
    uint16_t numGlobals;    // number of globals defined
    uint16_t numCommands;   // number of commands defined
    uint16_t numAttributes; // number of attributes
    uint16_t numGroups;     // number of object groups
    uint16_t unknown;
    uint16_t invTop;        // inventory window bounds
    uint16_t invLeft;
    uint16_t invHeight;
    uint16_t invWidth;
    uint16_t invOffsetY;    // positioning offset for
    uint16_t invOffsetX;    // new inventory windows
    uint16_t defaultFont;   // default font
    uint16_t defaultSize;   // default font size
    uint8_t  attrIndices[]; // attribute indices into attribute table
    uint16_t attrMasks[];   // attribute masks
    uint8_t  attrShifts[];  // attribute bit shifts
    uint8_t  cmdArgCnts[];  // command argument counts
    uint8_t  commands[];    // command buttons
};
cmdArgCnts specify how many selected objects a command requires.
commands determines whether or not a command has a corresponding button control. If the byte is 0, then there is not a button. The button controls correspond to 0x81+index.

Diploma geometry $81

This file specifies the geometry and size of the "signature" on the diploma.
struct DiplomaGeometry {
    uint16_t signatureFont; // font of the signature
    uint16_t signatureSize; // font size of the signature
    uint16_t top;           // bounds of the signature text box
    uint16_t left;
    uint16_t bottom;
    uint16_t right;
};

Text Huffman table $83

This is the huffman table for text compression. It is not always present. If this table is not present, the text compression uses the older 1.0 compression.
struct Huffman {
    uint16_t numEntries;  // number of entries in table
    uint16_t reserved;
    uint16_t masks[];     // bitmasks for each entry
    uint8_t  lengths[];   // bitlengths of each entry
    uint8_t  values[];    // values for each entry
};

MENU format

struct MENU {
    uint16_t menuID;      // id of the menu
    uint16_t width;       // used in memory
    uint16_t height;      // used in memory
    uint16_t resourceID;
    uint16_t placeholder;
    uint32_t enabled;     // bitmask for enabling menu items
    uint8_t  title_length;
    char     title[];     // title of menu
    MenuItem items[];     // the menu items
};
struct MenuItem {
    uint8_t name_length;
    char    name[];      // name of menu item
    uint8_t icon;        // icon ID for menu item
    uint8_t key;         // keyboard shortcut
    uint8_t mark;        // checkmark for item
    uint8_t style;       // menu item style
};
The menu items loop until one has a name_length of 0, signifying the end of the menu.

STR format

struct STR {
    uint8_t length;
    char    string[];    // text string
};

STR# format

struct STR_Table {
    uint16_t numStrings; // number of strings in table
    STR      strings[];  // strings in table
};
Convention says that the strings array is 1-indexed. So the first string is known as string 1.

WIND format

struct WIND {
    uint16_t top;     // window bounds
    uint16_t left;
    uint16_t bottom;
    uint16_t right;
    uint16_t type;    // window type, see below
    uint16_t visible; // 0 = invisible
    uint16_t close;   // 0 = no close box
    uint32_t refcon;  // Reference Constant
    uint8_t  title_length;
    char     title[]; // window title
};

// window types	
#define document    0x00
#define dBox        0x01
#define plainDBox   0x02
#define altDBox     0x03
#define noGrowDoc   0x04
#define movableDBox 0x05
#define zoomDoc     0x08
#define zoomNoGrow  0x0c
#define rDoc16      0x10   //16 pixel rounded corners
#define rDoc4       0x12   //4 pixel rounded corners
#define rDoc6       0x14   //6 pixel rounded corners
#define rDoc10      0x16   //10 pixel rounded corners

Container Format

The data files containing graphics, text, sounds, and other game data all use the same container format. This format packs multiple objects into a single file. The container has 3 regions: a file header, a data block, and an index into the data area.

Container Header

struct Container_Header {
    uint32_t magic;
};

If magic has the high bit set, then this is the standard container format. If magic doesn't have the high bit set, then this is a simplified container format.

For the simplified container format, magic contains the length of each object in the file. There is no index, since all the objects are the same length. Figuring out the offset of a specific object is as simple as multiplying the object number by the size of the object and addng the header size. The number of objects in the file can be deduced by dividing the file length (minus the the size of the header) by the object length. The objects start immediately after the header, and are stored one after the other, in order.

For the standard container format, the lower 31 bits of magic contain the offset of the SubHeader, from the beginning of the file.

Container SubHeader

struct SubHeader {
    uint16_t numObjects; // number of objects in the file.
    uint16_t huff[15];   // huffman masks
    uint8_t  lens[16];   // huffman lengths
    Group    groups[];   // index groups
};
struct Group {
    uint24_t bitoffset;  // uint24 is 3 bytes.
    uint24_t offset;
};

Each group represents 64 objects. There is a length table following the SubHeader, it is huffman compressed. The bitoffset in each group points to the start of that group's length table from the start of the SubHeader. Note that this is a bit offset, not a byte offset, so a group's length table can start halfway through a byte. Each group has 64 entries in its length table, corresponding to the 64 objects the group contains. The group offset, which is relative to the end of the container header, points to the start of the object data for the objects in that group.

The way to read the length table is to peek at 16 bits from the length table, then loop through huff[] looking for the first entry that is larger than the 16bits you just got. Then you can use the corresponding value in the lens[] array to determine how many bits you should read from the length table. That will give you the length of the object.

Each object is stored consecutively starting at its group's offset. So you'll need to read the lengths of any objects in the group that come before the object you're trying to find and add their lengths to the offset to calculate your object's offset.

The following pseudocode might be easier to understand.

Container Pseudocode

   ;  objectID is the object we wish to find.
   magic = read32()           ;read 4 byte magic

   if not magic & $80000000 then:  ;simple container?
       length = magic
       seek(objectId * length + 4) ;seek to object
       object = read(length)       ;read the object
       return object

   subHead = magic & $7fffffff
   seek(subHead)               ;seek to subheader
   numObjects = read16()

   if objectID >= numObjects then:
       error.

   huff = read16(15)           ;read 15 word huffman table
   lens = read(16)             ;read 16 byte huffman bitlengths
   group = objectId >> 6       ;find group for object
   index = objectId & $3f      ;find index into group for object

   seek(subHead + group*6 + $30) ;seek to appropriate group
   bitoffset = read24()
   offset = read24()

   bitReader.seek(subHead + bitoffset >> 3) ;seek to group length table
   bitReader.read(bitoffset & 7)      ;throw away the first X bits

   length = 0
   for i = 0 to index:              ;get lengths of all prior objects
       offset += length             ;advance object offset
       mask = bitReader.peek(16)    ;peek at the next 16 bits
       for j = 0 to 15:
           if huff[j] > mask then stop.
       length = bitReader.read(lens[j]) ;get length

   seek(offset + 4)                 ;seek to object
   object = read(length)            ;read the object
   return object

Text Format

The text is stored in the standard container format. Each object in the container is a text string, compressed with either a huffman compression if the GNRL83 resource is present, or with an older 5-bit character compression if it is not.

One thing to note, if the engine requests text with an ID that has the high bit set (bit 15), then it is actually requesting the last input string, not a text string encoded here.

Old Text Format

The old text format was used in the earlier versions of the MacVenture engine. It is also used in the Apple IIgs port.

The first 16 bits of the object contain the length of the string. Following this, are a series of 5-bit characters, packed together. Since the characters are only 5 bit, case is not encoded as part of each character. Instead there is a lowercase flag that gets toggled via control characters. At the beginning of the string, the lowercase flag is OFF, meaning the first character of the string is uppercase if it is not a control character.

The following table shows the meaning of each 5-bit character, depending on the state of the lowercase flag. (Yes, the period and comma are reversed to what you would expect).

5-bit ValueValue (lowercase)Value (uppercase)
$00[space][space]
$01-$1aa-zA-Z
$1b[period][comma]
$1c[apostrophe][double quote]
$1dComposite, see below
$1e8-bit character, see below
$1fFlip lowercase flag

Characters $01-$1e all enable the lowercase flag after you process the character.

$1d is a composite identifier. The ID is found in the next 16 bits in the stream. If the high bit is not set (bit 15), then this ID represents a text ID that should be fetched and inserted into the string. If the high bit is set, then the inverse of this ID represents a Composite Object String, which you should parse via the rules defined below, after you xor the ID with $ffff.

$1e means that an 8-bit ASCII character follows. Read the next 8 bits in the stream and append that ASCII character to the string.

$1f toggles the lowercase flag. It outputs nothing. If the lowercase flag is ON, $1f turns it off and vice-versa.

New Text Format

The new text format is huffman encoded. It works pretty much the same way the Container indices are compressed.

First read 1 bit. If this bit is set, then the next 15 bits are the length of the string, otherwise the next 7 bits are.

For each character in the string, you'll want to peek at the next 16 bits in the stream. Then loop through the masks table defined by GNRL83 for the first entry that is larger than the 16 bits you just peeked. The equivalent entry in the GNRL83 lengths array specifies how many bits to advance the stream (you should do that immediately). The equivalent entry in the GNRL83 values array specifies the character.

CharacterDescription
$017-bit ASCII, see below
$02Composite, see below
*ASCII, embed as-is

$01 means that a 7-bit ASCII character follows. Read the next 7 bits in the stream and embed those in the string.

$02 means that there's a composite string. You should read the next bit in the stream, if this bit is set, then the next 15 bits of the stream represent a Text ID you should fetch and embed. If the bit wasn't set, then the next 8 bits of the stream represent a Composite Object String, you should parse this via the rules below.

All other characters should be output as they are.

Composite Object Strings

The Composite ID contains several flags that determine how to select the proper object name:
BitsDescription
0-1NameType
2Capitalize
3ObjectSelector

As you'll discover later, the engine has a Target and a Source object set. If ObjectSelector is set, we'll use the Target object for the following operations, otherwise we'll use the Source object.

If NameType is $01, then instead of the name of the selected object, we want the indirect article (he, she, it). To find this, we fetch the 7th attribute for the object (we'll see how to do this later). Bits 4-5 of this attribute contain an index into STR#84. Remember, convention says that string tables are 1-based, so add 1 to this index.

If NameType is $02 or $03, then we want to get the name of the selected object, and prepend the appropriate object prefix.

To get the name of the object, look up the text string with the same ID as the selected object ID. Next we need to get the prefix. We first get the 7th attribute of the object. Bits 0-1, and bits 2-3 of this attribute contain the prefix indicies. If NameType is $02, we'll use bits 0-1, otherwise we'll use bits 2-3. If those bits are 0 then there is no prefix. Otherwise, those bits contain an index into STR#83. This prefix index is already 1-based, so no need to add 1 to it.

If Capitalize is set, you should then word capitalize the resulting composite.

Graphics Format

There are 2 graphics formats used. Older versions of MacVenture used the PACK format for the title screen and splash screen, and PPIC for the in-game graphics. Newer versions use PPIC format for everything.

You can tell the difference between the older version and newer versions by checking for the presence of a PPIC resource in the title file. If it is missing, then the data fork contains a PACK image.

The in-game graphics are interleaved between the image and its mask. For example, object #3 has its image at 3 * 2 = 6, and its mask at 3 * 2 + 1 = 7. If a graphic is only 2 bytes long, then those 2 bytes represent the ID of the actual graphics object. This way multiple objects can have the same graphic representation.

PACK Format

This PACK format is used for the title screen and splash screens of the older versions.

The PACK format is always 512x302. The first $1ff bytes of the pack format are unknown. Seek to $200 and start decoding from there. Pixels are stored in RLE.

Read a byte, if the byte is $80, then it's a NO-OP, do nothing but move to the next pixel. If the byte has the high bit set (bit 8), then the inverse of the byte, plus 2, is the count, and the next byte is repeated count times. Otherwise, the byte plus one is the count, and you should copy the next N bytes to the output.

Each line is 576 pixels wide, and you should copy the the first 512 pixels to the output, and discard the rest. Repeat for all 302 lines.

The following pseudocode should help.

    repeat 302 times:             ; 302 scanlines
        count = 0
        while count < $48:        ; 576 pixels at 1bpp
            n = nextbyte()
            if n == $80:
                noop.
            elsif n & $80:
                n = (n ^ $ff) + 2 ; get count
                v = nextbyte()    ; the pixel value to copy
                repeat n times:
                    line[count++] = v
            else:
                n++
                repeat n times:
                    line[count++] = nextbyte()
       for i = 0 to $40:          ; copy 512 pixels
           image[out++] = line[i]

PPIC Format

The PPIC format uses multiple types of compression. The first 3 bits determine the compression mode. Read those in and save for later. Next is the height and width of the image. To read those in, first read 1 bit. If this bit is set, then the next 10 bits are the height, otherwise the next 6 bits are the height. Then repeat the process for the width.

You will need to calculate the image stride after you have the width. The stride is the byte width, rounded up to the nearest 16-bit boundary. So add 15 to the width, divide by 16, then multiply by 8.

Now you can use the compression mode setting to decode the rest of the PPIC.

PPIC Mode 0

This is the simplest of the PPIC modes. It is uncompressed, and instead each byte is stored directly, left to right, top to bottom. However, since we're not necessarily byte aligned at this point, you'll need to be able to handle reading these bytes as part of a bitstream.

PPIC Mode 1

Mode 1 uses a fixed huffman table. The table itself is included here.

MaskLengthValue
$0000$03$00
$2000$03$0f
$4000$04$03
$5000$04$05
$6000$04$06
$7000$04$07
$8000$04$08
$9000$04$09
$a000$04$0a
$b000$04$0c
$c000$04$ff
$d000$05$01
$d800$05$02
$e000$05$04
$e800$05$0b
$f000$05$0d
$f800$05$0e

Using this huffman table, please see the Huffman Image Decoding section on how to use it.

PPIC Mode 2

Mode 2 is just like Mode 1, only it uses a different huffman table.

MaskLengthValue
$0000$02$ff
$4000$02$00
$8000$02$0f
$c000$05$01
$c800$05$03
$d000$05$07
$d800$05$0e
$e000$05$0c
$e800$05$08
$f000$06$06
$f400$07$02
$f600$07$04
$f800$07$09
$fa00$07$0d
$fc00$07$0b
$fe00$08$0a
$ff00$08$05

Using this huffman table, please see the Huffman Image Decoding section on how to use it.

PPIC Mode 3

Mode 3 uses a custom huffman table, stored at the beginning of the PPIC. Loading the huffman table is a little complicated. First you need to populate the value table using the following table. Reading the appropriate number of bits, and storing the calculated values in the proper locations in the array.

Bits to readValueValue2Value3
8[0] = v/15[2] = v%15
4[1] = v
7[3] = v/9[8] = v%9
4[4] = v
10[5] = v/77[6] = (v/7)%11[10] = v%7
6[7] = v/6[11] = v%6
3[9] = v
4[12] = v/3[14] = v%3
2[13] = v
1[15] = v

Next, we need to normalize the value table with the following pseudocode.

    values[16] = 0
    for i = 16 to 1:
        for j = i to 16:
            if values[j] >= values[i-1] then:
                 values[j]++
    for i = 16 to 0:
        if values[i] == $10 then:
             values[i] = $ff
             stop.

Finally we read in the huffman lengths and masks using the following pseudocode.

    bits = readbits(2) + 1
    mask = 0
    for i = 0 to 15:
        if i != 0 then:
            while !readbits(1) do:
                bits++
        lengths[i] = bits
        masks[i] = mask
        mask += 1 << (16 - bits)
    masks[15] = mask
    while mask & (1 << (16 - bits)) do:
        bits++
    masks[16] = mask | (1 << (16 - bits))
    lengths[15] = lengths[16] = bits

This builds the proper huffman table we can then use to decode the PPIC image. See Huffman Image Decoding below for how to use this table.

Huffman Image Decoding

Using the huffman table for image decoding is more complicated than what we've seen previously with text decoding. So I'll be using pseudocode to describe it.

walkHuffRepeat = 0
walkHuffLast = 0
edge = width & 3
if edge then:
    flags = readbits(5)
else
    flags = readbits(4) * 2
odd = 0
blank = width & 15
if blank then:
    blank /= 4
    odd = blank & 1
    blank = 2 - blank/2
p = 0
for y = 0 to height-1:
    for x = 0 to (width/8)-1:
        hi=walkHuff()
        image[p++] = walkHuff() | (hi << 4)
    if odd then:
        image[p] = walkHuff() << 4
    p += blank
if edge then:
    p = stride - blank
    bits = 0
    val = 0
    for y = 0 to height-1:
        if flags & 1 then:
             if bits < edge then:
                 v = walkHuff() << 4
                 val |= v >> bits
                 bits += 4
             bits -= edge
             v = val
             val <<= edge
             val &= $ff
        else:
             v = readbits(edge)
             v <<= 8-edge
        if odd then:
             v >>= 4
        image[p] |= v & $ff
        p += stride
if flags & 8 then:
    p = 0
    for y = 0 to height-1:
        v = 0
        if flags & 2 then:
            for x = 0 to stride-1:
                image[p] ^= v
                v = image[p++]
        else:
            for x = 0 to stride-1:
                val = image[p] ^ v
                val ^= (val >> 4) & $f
                image[p++] = val
                v = val << 4
if flags & 4 then:
    delta = stride * 4
    if flags & 2 then:
        delta *= 2
    p = 0
    q = delta
    for i = 0 to height * stride - delta:
        image[q++] ^= image[p++]

and the walkHuff function:

if walkHuffRepeat then:
    walkHuffRepeat--
    walkHuffLast = swap16(walkHuffLast)
    return walkHuffLast & $ff
dw = peeknext16bits()
for i = 0 to 15:
    if huffman.masks[i+1] > dw then stop
seekbits(huffman.lengths[i])
v = huffman.values[i]
if v == $ff then:
    if not readbits(1):
         walkHuffLast &= $ff
         walkHuffLast |= walkHuffLast << 8
    walkHuffRepeat = readbits(3)
    if walkHuffRepeat < 3 then:
         walkHuffRepeat <<= 4
         walkHuffRepeat |= readbits(4)
         if walkHuffRepeat < 8 then:
             walkHuffRepeat <<= 8
             walkHuffRepeat |= readbits(8)
    walkHuffRepeat -= 2
    walkHuffLast = swap16(walkHuffLast)
    return walkHuffLast & $ff
else:
    walkHuffLast <<= 8
    walkHuffLast |= v
return v

Object Drawing

Drawing an object isn't quite as simple as just fetching its graphic and blitting it to the view window, but it's not super complicated either.

There are multiple drawing modes used. Namely BIC, OR, and XOR. The table below shows how each mode sets pixels based on the graphic data.

ModeSourceDestination
OR0Unchanged
OR1Set to black
XOR0Unchanged
XOR1Inverted
BIC0Unchanged
BIC1Set to white

When the engine tells you to draw an object, you are given the object ID as well as a drawing style.

First, check to see if the object has a mask. If it does, then either draw the mask using BIC (if style is 1) or using OR (if style is 2). If there is no mask present, calculate the dimensions of the image and either fill in the rectangle with white (if style is 1) or black (if style is 2).

Next, if style is not 0 and an image is present, draw the image using XOR.

For drawing the title screen, you can fill the screen with white, and OR the image over it.

Sound Format

Unfortunately the sound format isn't as straight-forward as the other formats. This is because each sound basically has a chunk of 68k code at the beginning of it that is run to play the specific sound. We can work around this limitation, however, by recognizing a signature character and doing the appropriate thing.

The signature character is located at offset 5. That character determines how to read the sound data. See the sections below for the different formats for each value of offset 5.

Sound $10

struct Sound10 {
    uint8_t  assembly[0x198];
    uint8_t  wavetable[16];
    uint32_t sampleLength;
    uint16_t unknown;
    uint32_t frequency;
    uint8_t  wavedata[];
};

Since the wave data is stored as 4bit audio, sampleLength needs to be doubled to accurately reflect the sample length.

The frequency is scaled relative to 22100 hz. So multiply by 22100 and divide by 0x10000 in order to calculate the actual frequency.

Finally, loop through the wave data and look up each 4-bit sample in the wave table to have an accurate 8bit wave.

Sound $12

struct Sound12_head {
    uint8_t  assembly[0xc];
    uint16_t repeat;
    uint8_t  assembly[0x26];
    uint16_t base;
}
struct Sound12_info {  //starts at base+0x34
    uint32_t sampleLength;
    uint16_t unknown;
    uint32_t frequency;
}

This sound repeats, and each repeat has a volume scale. The scales are 16-bit values located at 0xe2. The sound data itself starts at base + 0xa. Each sound sample is signed 8-bit, and should be multiplied with the volume scale and then divided by 256. Thus the output sound should be sample length * repeat bytes long.

The frequency is scaled to 22100 hz, just like sound $10.

Sound $18

struct Sound18 {
    uint8_t  assembly[0x252];
    uint8_t  wavetable[16];
    uint32_t sampleLength;
    uint16_t unknown;
    uint32_t frequency;
    uint8_t  wavedata[];
}

This sound is handled identically to sound $10.

Sound $1a

struct Sound18 {
    uint8_t  assembly[0x220];
    uint8_t  wavetable[16];
    uint32_t sampleLength;
    uint16_t unknown;
    uint32_t frequency;
    uint8_t  wavedata[];
}

This sound is handled identically to Sound $10

Sound $44

struct Sound44 {
    uint8_t  assembly[0x5e];
    uint32_t sampleLength;
    uint32_t frequency;
    uint8_t  wavedata[];
}

The wave data is signed 8-bit. Frequency is scaled just like Sound $10.

Sound $78

struct Sound78 {
    uint8_t  assembly[0xba];
    uint8_t  wavetable[16];
    uint32_t unknown;
    uint32_t sampleLength;
    uint32_t frequency;
    uint8_t  wavedata[];
}

This is handled almost identically to Sound $10. sampleLength is correct, no need to scale it. In Sound $10, you process the low-nibble first, in Sound $78, you process the high-nibble first.

Sound $7e

struct Sound7e {
    uint8_t  assembly[0xc2];
    uint8_t  wavetable[16];
    uint32_t unknown;
    uint32_t sampleLength;
    uint32_t frequency;
    uint8_t  wavedata[];
}

This is handled similar to Sound $78, except that the wavetable contains deltas. So start with a value of $80. Then we add the delta from the wavetable to the value and put it in the output stream. Then we use that new value for the next sample.

Game Data Format

In the game, there are global variables and objects. Objects are made up of attributes. Some attributes are constant and cannot be changed. The attributes that can be changed are stored in the Game Save, the attributes that are constant are stored in the Object file. Global variables are always stored in the game save, since all global variables can be changed.

When you start a new game, the engine loads a special game save that contains the initial settings for all the attributes and global variables, this special save file name is located in STR85.

We'll cover the game save format first.

Save Format

The game save is divided up into 3 parts. The first part contains the object attributes. The second contains the global variables. The third, optional, part is the contents of the text window. The number of objects and globals that are stored in the game save are found in GNRL80.

struct SaveGame {
     Group groups[numGroups];
     uint16_t globals[numGlobals];
     char text[];
}
struct Group {
     uint16_t attributeValue[numObjects];
}

Because only the dynamic attributes are stored in the game file, the objects are cut up into groups. The number of groups can also be found in GRNL80. Each group corresponds to a single attribute, and contains that attribute for all of the objects in the game.

All of the global variables are stored in the globals array, and text contains the contents of the text window, if present. You can determine whether or not text is present by the size of the file.

Objects Format

The objects file contains all the constant attribute data for the game objects. It is stored in the simplified container format. Each object is a packed array of attributes. We'll see in the next section how to read those attributes.

Attributes

Each object contains several attributes, these attributes define the object name, its parent, which exit it connects to if it's a door, etc.

In GNRL80, we have 3 arrays, attrIndices, attrMasks, and attrShifts. These arrays define how and where attributes are stored. In GNRL80 we can also find the number of attributes.

The first thing we do, is given a specific attribute, we look up its Index in attrIndices. If the high bit of this Index value is set, then this attribute is a constant. If this attribute is not a constant, then the index corresponds to the Group Number of the attribute.

If the attribute is a constant, then the low 7-bits of the index become an index into the object's attribute array. This array is composed of 16-bit values.

After we fetch the word value containing the attribute, either from its Group, or from the object's attribute array, we need to mask out the bits we want using attrMasks, and then right shift the result by the number of bits specified in attrShifts. The end result is the attribute we want. This end result should be returned as a signed word.

The following pseudocode shows how to read an object's attribute.

    ; object = the object ID we want to get the attribute for
    ; attr = the attribute ID
    index = attrIndices[attr]
    if not index & $80 then:  ; dynamic attribute?
        val = savegame[index][object]
    else:
        array = getObject(object) ; fetch object attribute list
        if array.length == 0 then return 0
        index &= 0x7f          ; strip off high bit
        ; get the appropriate word from the attribute list
        val = (array[index * 2] << 8) | array[index * 2 + 1]
    val &= attrMasks[attr]
    return val >> attrShifts[attr]

The first 11 attributes have specific purposes, the rest of the attributes can be used for whatever the game desires. The following table lists the known attribute definitions.

AttributeDefinition
0Parent Object
1X position in window
2Y position in window
3Invisible flag
4Unclickable flag
5Undraggable flag
6Container Open
7Prefixes
8Is Exit
9Exit X
10Exit Y
11Hidden Exit
12Other Door
13Is Open
14Is Locked
16Weight
17Size
19Has Description
20Is Door
22Is Container
23Is Operable
24Is Enterable
25Is Edible

Attributes 0-11 are used by the engine. The other attributes are game-defined, but I've listed the most common usage of those other attributes. Other Door is the ID of the connecting door in the other room.

Finally, object 1 is the player. So object 1's parent is the current room. Object 0 is "the world", a global object that is used as a general unseen container. (For example, when "destroying" an item, it's often removed from the player and placed in "the world").

Commands

In theory, the command meanings are purely up to the script engine. In practice, some of the commands are hardcoded into the interpreter, and have a fixed meaning, and the rest, while they could mean anything, all the ICOM games use the same meaning.

Below is a list of the command ID and its meaning.

Command IDDefinition
0No command selected
1Start or Resume game
2Close
3Tick
4Activate Object
5Move Object
6Consume
7Examine
8Go
9Hit
10Open
11Operate
12Speak
13Babble
14Target Name
15Debug Object

When the game is starting or we're loading a saved game, Command #1 is called. The selected object is the current room.

Tick is called after we finish running any other command, except for Command #1. For example, if you "Operate", Command #11 is run for each selected object, and then Tick is called.

Activate Object is called whenever we double-click an object. It basically runs the "default" action for that object (doors open if closed, etc).

Move Object is called whenever an object is dragged to a new position. The x/y values passed to the engine contain the delta x/y. The Target Object contains the target window ID. If you're dragging objects between windows, the delta should be calculated as if the windows were right on top of each other.

Babble is activated by the script after what you have said in a speech dialog has passed the profanity filter.

Target Name simply prints the name of the target object to the text window. It is called from other scripts.

Debug Object prints information about the currently selected object. It is never called, but you can write your own button that triggers it. The selected object is the object to inspect, and Target Object should be set to the selected object's parent.

All the other commands correspond to buttons in the command window.

Script Format

The scripts are stored in the Filters file. This file uses the standard container format. Each object in the game has a script attached to it (script ID corresponds directly to object ID). Script 0 is the main dispatch script, this script is called whenever the user does something, as well as when the game first runs.

Following the execution of the dispatch script, the scripts for the current room as well as all of the current room's children get run. Finally, any delayed scripts get run based on priority.

The script engine is passed 4 objects every time it is run. The first is the currently selected command, if any. Next is the currently selected object. If more than one object is selected, then the engine is called for each selected object. Next is the target object, this might be the window ID of a dropped object or it might be the object selected to complete a command, or it might be nothing. Finally the mouse delta from a dragged object is passed, if any.

If you recall earlier, we were talking about a source and destination objects in the text decoding section. The selected object is the source, the target object is, of course, the target.

So, we load the proper script, be it an object script or the dispatch script, and then we initialize the engine. The engine is a stack-based engine. The stack is a stack of signed words. There are no registers, just a stack, and a saved-script location which contains delayed scripts and their priorities. The script will save scripts for later execution, and give them a priority. The engine will run those saved scripts in order from highest priority to lowest priority after it runs all the other scripts it needs to.

The scripts themselves are a series of instruction opcodes, each 1 byte long, as well as any immediate data that may or may not be present. If the high bit of an opcode is not set, then we simply push that value onto the stack. If the high bit is set, then we need to decode that opcode and execute it.

Get Attribute $80

Pops the object ID, followed by the attribute ID. Gets that object's attribute, and pushes it.

Set Attribute $81

Pops the object ID, the attribute ID, and the Value. Sets the object's attribute to the value.

Sum Family Attribute $82

Pops the object ID, the attribute ID, and a recursion flag. Fetches the object's attribute, as well as its children attribute, and possibly children's children's attribute if recursion flag is not 0, and adds them together. Pushes the result.

Push Command $83

Pushes the command ID that was passed to the script engine.

Push Source $84

Pushes the currently selected object that was passed to the script engine.

Push Target $85

Pushes the target object that was passed to the script engine.

Push Delta X $86

Pushes the X coordinate of the drag delta that was passed to the script engine.

Push Delta Y $87

Pushes the Y coordinate of the drag delta that was passed to the script engine.

Push Immediate.b $88

Reads the byte following the opcode, and pushes it onto the stack.

Push Immediate $89

Reads the word following the opcode, and pushes it onto the stack.

Get Global $8a

Pops the global ID, fetches the value from the global variables array, and pushes it onto the stack.

Set Global $8b

Pops the global ID and the Value. Sets the corresponding global variable to the value.

Random $8c

Pops the maximum bound. Pushes a random integer from 0 up to, but not including, the maximum bound.

Copy $8d

Peeks at the top of the stack, pushes a copy of the value onto the stack.

Copy N $8e

Pops the number N from the stack. Peeks at the N top-most values of the stack and pushes copies of them. Calling this with N = 1 is identical to opcode $8d.

Swap $8f

Swaps the top 2 values on the stack.

Swap N $90

Pops the number N from the stack. Swaps the top of the stack with the value N positions from the top. Calling this with N = 1 is identical to opcode $8f.

Pop $91

Pop the topmost value from the stack and throw it away.

Copy + 1 $92

Peeks at the second-top-most value on the stack and pushes a copy of it.

Copy + N $93

Pops the number N from the stack. Peeks at the object at position N and pushes a copy on the stack. Calling this with N = 1 is identical to opcode $92.

Shuffle $94

This re-orders the top 3 objects on the stack. Pop A, B, and C from the stack. Push A, C, and B onto the stack.

Sort $95

Pop Step from the stack, followed by N. Sort the values on the stack, Step being the direction (+1 or -1), and N being the number of values from the top.

Clear $96

Empty the stack completely.

Get Stack Size $97

Push the number of elements in the stack, onto the stack.

Add $98

Pop B and A from the stack. Push A+B onto the stack.

Subtract $99

Pop B and A from the stack. Push A-B onto the stack.

Multiply $9a

Pop B and A from the stack. Push A*B onto the stack.

Divide $9b

Pop B and A from the stack. Push A/B onto the stack.

Modulo $9c

Pop B and A from the stack. Push A%B onto the stack.

DivMod $9d

Pop B and A from the stack. Push A%B and A/B onto the stack.

Absolute $9e

Pop a signed value from the stack. Push the absolute value onto the stack.

Negate $9f

Pop a signed value from the stack. Push the negative of this value onto the stack.

Binary And $a0

Pop B and A from the stack. Push A&B onto the stack.

Binary Or $a1

Pop B and A from the stack. Push A|B onto the stack.

Binary Xor $a2

Pop B and A from the stack. Push A^B onto the stack.

Binary Not $a3

Pop a value from the stack. Push the bitwise NOT onto the stack.

And $a4

Pop B and A from the stack. If neither A nor B are zero, push -1 onto the stack, otherwise push 0.

Or $a5

Pop B and A from the stack. If either A or B are not-zero, push -1 onto the stack, otherwise push 0.

Xor $a6

Pop B and A from the stack. If A is not-zero and B is zero, or if B is not-zero and A is zero, push -1 onto the stack, otherwise push 0.

Not $a7

Pop a value from the stack. If the value is zero, push -1 onto the stack, otherwise push 0.

Greater Than Unsigned $a8

Pop B and A from the stack. If A is greater than B, push -1 onto the stack, otherwise push 0. A and B are both treated as unsigned.

Less Than Unsigned $a9

Pop B and A from the stack. If A is less than B, push -1 onto the stack, otherwise push 0. A and B are both treated as unsigned.

Greather Than $aa

Pop B and A from the stack. If A is greater than B, push -1 onto the stack, otherwise push 0.

Less Than $ab

Pop B and A from the stack. If A is less than B, push -1 onto the stack, otherwise push 0.

Equal $ac

Pop B and A from the stack. If A is equal to B, push -1 into the stack, otherwise push 0.

String Equal $ad

Pop B and A from the stack. Look up the strings corresponding to those IDs. If the two strings are the same, push 1 onto the stack, otherwise push 0.

Contains $ae

Pop Needle and Haystack from the stack. Look up the strings corresponding to those IDs. If haystack contains needle, regardless of case, then push 1 onto the stack, otherwise push 0.

Contains Word $af

This is similar to opcode $ae, except that haystack must contain needle as a word, not as part of a larger word. So if haystack contained "hello mom", the needle "hell" does not match, but the needle "hello" does. With success, you push -1 onto the stack.

Branch $b0

Reads the signed word following the opcode, adds this word to the current opcode offset. Continues execution from there.

Branch.b $b1

Reads the signed byte following the opcode, adds this byte to the current opcode offset. Continues execution from there.

Branch if True $b2

Reads the signed word following the opcode. Pops a value from the stack, if this value is not equal to 0, adds the word to the current opcode offset and continues execution from there.

Branch if True.b $b3

Reads the signed byte following the opcode. Pops a value from the stack, if this value is not equal to 0, adds the byte to the current opcode offset and continues execution from there.

Branch if False $b4

Reads the signed word following the opcode. Pops a value from the stack, if this value is equal to 0, adds the word to the current opcode offset and continues execution from there.

Branch if False.b $b5

Reads the signed byte following the opcode. Pops a value from the stack, if this value is equal to 0, adds the byte to the current opcode offset and continues execution from there.

Call Later $b6

Pops a priority and a function id from the stack. Pushes these onto the saved function stack to call later.

Cancel Later $b7

Pops a function id from the stack. Removes this function from the saved function stack.

Cancel Lower Priority $b8

Pops a priority from the stack. Removes any functions in the saved function stack that have a priority less than or equal to the popped priority.

Cancel Higher Priority $b9

Pops a priority from the stack. Removes any functions in the saved function stack that have a priority greater than or equal to the popped priority.

Cancel Priority Range $ba

Pops a max priority and a min priority from the stack. Removes any functions in the saved function stack that have a priority that falls between these values, inclusively.

Fork $bb

Pops a new active command, new source object, new target, new drag X and new drag Y from the stack. Calls the engine using these new values.

Call $bc

Pops a function Id from the stack. Calls that function with the current stack.

Focus Object $bd

Pops an object id from the stack. Gives the object id a queueId of 2, and pushes it onto the Object Queue. See the Queues below for more information.

Change Rooms $be

Pops FromRoom and ToRoom from the stack. Gives these rooms a queueId of 8 and pushes them onto the Object Queue. See the Queues below for more information. Sets ToRoom's Attribute 6 to FromRoom's Attribute 6. Sets FromRoom's attribute 6 to 0. Moves all of FromRoom's children to ToRoom.

Snap Object $bf

Gives the current Source a queueId of 14 and pushes it onto the Object Queue. See the Queues below for more information.

Toggle Exits $c0

Pushes an empty object with a queueId of 13 onto the Object Queue. See the Queues below for more information.

Print Text $c1

Pops a text ID from the stack. Gives it a queueId of 3, and pushes it onto the TextQueue along with the current Source and current Target.

Print Newline $c2

Pushes an empty object with a queueId of 2 onto the TextQueue. See Queues below for more information.

Print Text and Newline $c3

Basically the same as opcode $c1 followed by opcode $c2.

Print Paragraph $c4

Basically the same as opcode $c2, followed by opcode $c1, followed by opcode $c2 again.

Print Number $c5

Pops a number from the stack. Gives the number a queueId of 1 and pushes it onto the TextQueue. See Queues below for more information.

Push Unknown $c6

This is hardcoded to push the number 2 onto the stack. I'm not sure what this represents, possibly the OS identifier.

Play Background Sound $c7

Pops a sound ID from the stack. Gives it a queueId of 1 and pushes it onto the SoundQueue. See Queues below for more information.

Play Sound $c8

Pops a sound ID from the stack. Gives it a queueId of 2 and pushes it onto the SoundQueue. See Queues below for more information.

Wait Sound $c9

Pushes an empty object with a queueId of 3 onto the SoundQueue. See Queues below for more information.

Get Current Time $ca

Pushes the full year, the month (1=January), the day of month, the current hour, the current minute, and the current second onto the stack.

Get Current Day $cb

Pushes the current day of the week onto the stack. Sunday = 1.

Get Children $cc

Pops the recursion flag and object Id from the stack. Gets all the children of the object, and possibly its children's children if the recursion flag is set, and pushes their ids onto the stack. Lastly it pushes the number of children and grandchildren onto the stack.

Get Num Children $cd

Pops the recursion flag, and object Id from the stack. Pushes the number of children for the object, including its children's children if the recursion flag is set, onto the stack.

Get Engine Version $ce

Pushes the current engine version onto the stack. This is currently 86.

Get Scenario Number $cf

Pushes the scenario number onto the stack. This is the number after MCV. You can get this number from STR#81. Note that you're pushing the numerical value, not the character.

Push Unknown $d0

Again, I'm not sure what this is. It is currently hardcoded to always push the number 1. Maybe this is the OS identifer, and I don't know what opcode $c6 is.

Get Object Dimensions $d1

Pops the object ID from the stack. Calculates the width and height of the object from its image. Then pushes its width and height onto the stack.

Get Overlap Percent $d2

Pops objects B and A from the stack. Calculates the intersection of A's bounding rectangle and B's bounding rectangle. Then calculates the percent, from 0 to 100, of the area of the intersection to the area of A and pushes this onto the stack.

Capture Siblings $d3

Pops the object from the stack. Then finds all of the object's siblings that overlap it by at least 40% and makes them children of the object. This is mainly used for opening doors that have items hanging from them, when the door opens, the objects hanging on it disappear because they've been reparented to the door.

Release Siblings $d4

Pops the object from the stack. Then finds all of the object's children and makes them siblings of the object. This is mainly used when closing a door that has items hanging on it. When the door closes, the objects that were previously hidden, now reappear.

Show Speech Dialog $d5

Pops a text Id from the stack. Then displays DLOG84, setting param 1 of the dialog to the text from the stack, and waits for the dialog to close. If the user pressed cancel, 0 is pushed onto the stack, otherwise -1 is pushed.

Activate Command $d6

Pops the command from the stack. Then highlights the command button which has the the popped RefCon.

Lose $d7

Displays ALRT86 and waits for user input, the script engine is halted. This is called when you have died or otherwise lost the game.

Win $d8

Closes all windows, displays WIND85, draws the diploma, and prompts for your name and prints the diploma. The script engine is halted.

Sleep $d9

Pops a length of time. This is the number of ticks to sleep until continuing on. There are 60 ticks in a second.

Pause $da

Flushes all queues, displays the Click To Continue banner, and waits for user input before continuing. Then hides the click to continue banner. See Queues below for more information.

Flush Object Queue $db

This flushes the Object Queue. See Queues below for more information.

Flush Sound Queue $dc

This flushes the Sound Queue. See Queues below for more information.

Flush Text Queue $dd

This flushes the Text Queue. See Queues below for more information.

Flush Queues $de

This flushes all queues. See Queues below for more information.

Flash Screen $df

Pops a length of time from the stack. Inverts the main window, and sleeps for the length of time specified.

Preload Graphic $e0

Pops an object from the stack. Loads the graphic for later use if it's not already loaded in memory.

Preload Sound $e1

Pops a sound Id from the stack. Loads the sound for later use if it's not already loaded in memory.

Multiply and Divide $e2

Pops B, A, and C from the stack. Multiples A and B and divides by C. Pushes the result onto the stack.

Update Window $e3

Pops an object Id from the stack. Determine the window that represents the object using the table below.

IDWindow
Current RoomMain Window
-1Command Window
-2Text Window
-3Self Window
-4Exit Window

If the object ID isn't in the table, then search for the window that has a RefCon equal to the Id. Bring the window to the front, run the object queue, and then redraw the window.

Sound Active $e4

Pushes 0 if there is no sound currently playing. Pushes 1 if we're currently playing a sound.

Wait For Sound $e5

Wait for sound to finish before continuing.

Get Fibonacci $e6

Push the result of opcode $e7 onto the stack.

Calculate Fibonacci $e7

Pops N from the stack. Calculate the Nth fibonacci number. Save it for opcode $e6.

I swear those last 2 are a joke. I've never seen them actually used.

Queues

The engine uses 3 queues. This way the scripts can queue up a bunch of changes and then have them happen all at once, or in sync. Each item in the queue has a queueId that simultaneously identifies the event, as well as gives it a priority. The 3 queues used by the engine are an Object Queue, which controls drawing, a Sound Queue which plays sounds, and a Text Queue which outputs text to the text window.

When you add something to a queue, you basically wrap it up and give it a queueId, then append it to the appropriate queue.

Object Queue

This queue runs in order of highest queueId down to the lowest. It is mostly used for updating the objects on the screen.

queueIdDescription
2Finds the window associated with the object and focuses it
3Opens the object's window. If it's a room, it updates the main window, if it's a container, it opens a new inventory window.
4Close's the object's window. If it's a room, the main window is cleared, but remains open. If it's a container, the inventory window corresponding to it closes.
7Checks the object for updating. If the object has changed, it updates the object on the screen.
8Finishes the room change. Updates the window titles, and redraws the window.
12Sets the attribute 6 of the old main window to 0, and sets the attribute 6 of the current player's parent to 1.
13Highlights the selected exits.
14Animates the object back to its original location.

Sound Queue

This queue runs in order of the items pushed. It is used for playing sounds.

queueIdDescription
1Plays a sound in the background
2Plays a sound, and waits for it to complete.
3Waits for a playing sound to complete.

Text Queue

This queue runs in order of the items pushed. It is used to update the text window.

queueIdDescription
1Appends a number to the text window
2Appends a newline to the text window
3Loads the text id and appends it to the text window

Other Platforms

Other platforms don't benefit from having a resource fork that contains all the structural information. So I'll document the other platforms that have unique solutions to this problem.

Apple IIgs

The IIgs has a few differences from the Macintosh version. One, the desktop is only 320x200. Two there are 16 colors instead of black and white.

RESOURCE.DAT

The Apple IIgs version has a file called RESOURCE.DAT which contains the window layouts, number of objects and global variables, along with some other customizations.

The format of RESOURCE.DAT is below. All multi-byte values are little endian.

OffsetSizeDescription
$0000$0aWindow main
$000a$0aWindow text
$0014$0aWindow exit
$001e$0aWindow commands
$0028$0aWindow self
$0032$0aWindow inventory
$003c$0aWindow diploma
$0046$02word diplomaSignatureBottom
$0048$02word diplomaSignatureLeft
$004a$02word diplomaSignatureWidth
$004c$02word numObjects
$004e$02word numGlobals
$0050$02word numAttributes
$0052$02word numGroups
$0054$02word unknown
$0056$02word numCommands
$0058$18byte cmdArgCnts[24]
$0070$40byte attrIndices[64]
$00b0$80word attrMasks[64]
$0130$40byte attrShifts[64]
$0170$20String graphicsPath
$0190$20String filterPath
$01b0$20String textPath
$01d0$20String objectPath
$01f0$20String gamePath
$0210$20String soundPath
$0230$20String titlePath
$0250$20String startupPath
$0270$20String diplomaPath
$0290$20String diplomaBWPath
$02b0$20String volumeName
$02d0$20word palette[16]
$02f0$100byte paletteMap[256]
$03f0$10String gameName
$0400$30Menu appleMenu
$0430$04dword unused
$0434$0eunknown
$0442$02Filetype of saved games
$0444$02Inventory background colors
$0446$02word unknown (passed to Desktop)
$0448$02word unknown (passed to Desktop)
$044a$0aColorTable windowColors
$0454$10Control scrollbar
$0464$04Control resize
$0468$50String credits1
$04b8$80String credits2
$0538$80String credits3
$05b8$80String certificatePrompt
$0638$02word Font ID
$063a$02word Font Size
$063c$28Pattern exitWindow background
$0664$28Pattern exitWindow selection bg
$068c$02Number of huffman items (optional)
$068e$02Reserved (optional)
$0690$bcHuffman masks[] (optional)
$074c$60Huffman lengths[] (optional)
$07ac$54Huffman values[] (optional)

The huffman elements are optional. If they are not present, then the RESOURCE.DAT file ends at $68c. If they are present then they represent the huffman tables for text decoding. If they aren't present, then the text decoder uses the older 1.0 encoding.

In the palette, the colors are formatted as such:

BitsColor
0-3Blue
4-7Green
8-11Red
12-150

The palette map is for translating colors. Every byte (2 pixels) from graphics are translatted through the paletteMap before being drawn on the screen.

Each Window contains the following data. Window dimensions do not include scrollbars.

OffsetSizeDescription
$0000$02word top
$0002$02word left
$0004$02word bottom
$0006$02word right
$0008$02word wFrameBits

wFrameBits:

BitDescription
0Ignored
1Window zoomed
2Internal usage
3Controls are active when window isn't
4Window has info bar
5Window is visible
6Window becomes active when content clicked
7Window can be moved
8Window has a zoom box
9Size of window is flexible
10Window has a grow box
11Window has horiz scrollbar
12Window has vert scrollbar
13Window has double frame
14Window has close button
15Window has titlebar

Each String contains the following data.

OffsetSizeDescription
$0000$01byte length
$0001*char string[]

Menus are formatted like so:
Each menu item starts with a space, each menu starts with ">" or $01. Then the letter "L" followed by the name of the menu or item. The name is terminated with a backslash "\". This is then followed by one or more control characters. Then finally the menu and menu items are terminated with $0d "\r".
The format is the menu is first, then the menu items inside that menu, the whole thing is terminated by the "." character.

The following is an example menu item.

">L File \\H\x02\r"
" LNew\\V*NnH\x01\x01\r"
" LOpen...\\*OoH\x02\x01\r"
" LSave\\*SsH\x03\x01\r"
" LSave As...\\VH\x04\x01\r"
" LQuit\\*QqH\x05\x01\r"
"."

The control characters are as follows:

CharacterDescription
*2 character follow, used as keyboard shortcuts
BBold
CCharacter follows is used as a checkmark
DDisabled
HWord that follows is item ID
IItalic
NSame as H, but a number string follows instead
UUnderline
VDivider follows this item
XColor-replace mode

So looking at our example, the Open menu item has a keyboard shortcut of Apple-O or Apple-o. And its ID is 0x0102. The Save-As menu item has a divider below it.

The Apple menu is identified with a name of "@".

ColorTable is formatted as follows:

OffsetSizeDescription
$0000$01High-nibble is the Window Border color
$0001$01Unknown
$0002$01Title text color (high-nibble=inactive, low=active)
$0003$01Inactive title background color (low nibble)
$0004$01Active title (high-nibble=stripe color, low=background)
$0005$01Active title style

Graphics

The IIgs graphics are stored using the same PPIC format as in the Macintosh version, with a few minor differences (The PACK format is not used at all). The first difference is that instead of 1 bit-per-pixel, the colors are 4 bits-per-pixel. The second difference is that right after the 3 mode bits, there are 2 unknown bits that you need to skip.

Each byte of the resulting bitmap needs to be translated through the paletteMap from RESOURCE.DAT. Then, each 4 bit nibble is looked up in the palette. On the Macintosh version, even-numbered graphics are the regular graphics, odd-numbered graphics are the masks. On the IIgs version, there are no odd-numbered graphics (they're all empty). The mask is built into the regular graphic image. Any pixels with an index of 0 are transparent.

Title

The startup and title graphics are stored in a fairly simple format.
OffsetSizeDescription
$0000$08Undocumented flags
$0008$80Palette
$0088$7d00Pixel Data

There are 16 colors in the palette, each color contains a lot of useless information.

OffsetSizeDescription
$0000$02Unused
$0002$02Red
$0004$02Green
$0006$02Blue

For each color channel, only the low byte is used. The color intensities range from $00 to $ff.

The pixel data is then a simple 320x200 run of palette indices, each one taking up 4 bits, high nibble first.

Diploma

The diploma is designed for the 640x200 IIgs mode. Because of this, and the fact that the dimensions aren't fixed, it requires slightly more work to decode.

The overall structure of the file is the same as for the startup and title screens. The palette is stored in the same format, except that instead of 16 entries, there are only 4. The pixel data is, therefore, 2 bits per pixel instead of 4 bpp. The high bits are still first, though.

To get the dimensions of the diploma, use the dimensions of the diploma Window in RESOURCE.DAT. The diploma graphic will fill that window perfectly.

Since the dimensions don't necessarily restrict each row of pixels to a byte boundary, there will be padding. The number of bytes per row is determined by (width+7)/4.

Sound

Thankfully, the sound assets on the IIgs aren't as screwed as on the Macintosh. The format is fairly straight-forward:

struct Sound {
    uint16_t numParts;
    SoundPart parts[];
    uint8_t sampleData[];
};
struct SoundPart {
    uint16_t sampleDataOffset; //from beginning of sound
    uint16_t sampleLength;     //in bytes
    uint16_t sampleType;       //see below
    uint16_t sampleFrequency;  //in hertz
};

A single sound is composed of multiple "parts". This allows sounds to have echoes, for example, without additional sample data simply by having multiple parts with overlapping sampleDataOffsets.

You might see that sampleFrequency is specific to the part and worry that means a single sound might use multiple frequencies. Luckily, none of the sounds in any game do this, and the IIgs game engine actually doesn't support it. The same goes for sampleType. Every part of a sound must have the same sampleFrequency and sampleType.

So to get the raw sound data, you stitch all the sampledata from all the parts together.

sampleType identifies how the sound is encoded. There are 3 types. Type 0 is straight 8-bit signed sampledata. Type 1 is MIDI data which I will not document here. Type 2 is compressed with a wavetable.

Type 0 is straightforward, and type 1 is MIDI, so let's look at type 2. The sampledata for type 2 sounds is composed of 4-bit indices into a wavetable. These indices are stored low-nibble first.

The wavetable is a static wavetable, shown below:

IndexValue
$00$7f
$01$7e
$02$7c
$03$78
$04$70
$05$60
$06$40
$07$01
$08$80
$09$81
$0a$83
$0b$87
$0c$8f
$0d$9f
$0e$bf
$0f$ff

So to build out the sample, you loop through the sampleData. For each nibble, look up the value in the wavetable and store it in a new array. The resulting array of bytes is your signed 8-bit sample. This array will also, naturally, be twice as long as the sampleData.