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
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.
From here, you can branch off and locate the other data files needed by the game. These files include:
The main executable resource fork contains several resources, the ones we care about are:
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. };
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
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 0x10The 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.
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 };
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.
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; };
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 };
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.
struct STR { uint8_t length; char string[]; // text string };
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.
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
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.
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.
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.
; 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
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.
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 Value | Value (lowercase) | Value (uppercase) |
---|---|---|
$00 | [space] | [space] |
$01-$1a | a-z | A-Z |
$1b | [period] | [comma] |
$1c | [apostrophe] | [double quote] |
$1d | Composite, see below | |
$1e | 8-bit character, see below | |
$1f | Flip 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.
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.
Character | Description |
---|---|
$01 | 7-bit ASCII, see below |
$02 | Composite, 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.
The Composite ID contains several flags that determine how to select the proper object name:
Bits | Description |
---|---|
0-1 | NameType |
2 | Capitalize |
3 | ObjectSelector |
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.
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.
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]
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.
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.
Mode 1 uses a fixed huffman table. The table itself is included here.
Mask | Length | Value |
---|---|---|
$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.
Mode 2 is just like Mode 1, only it uses a different huffman table.
Mask | Length | Value |
---|---|---|
$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.
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 read | Value | Value2 | Value3 |
---|---|---|---|
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.
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
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.
Mode | Source | Destination |
---|---|---|
OR | 0 | Unchanged |
OR | 1 | Set to black |
XOR | 0 | Unchanged |
XOR | 1 | Inverted |
BIC | 0 | Unchanged |
BIC | 1 | Set 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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
Attribute | Definition |
---|---|
0 | Parent Object |
1 | X position in window |
2 | Y position in window |
3 | Invisible flag |
4 | Unclickable flag |
5 | Undraggable flag |
6 | Container Open |
7 | Prefixes |
8 | Is Exit |
9 | Exit X |
10 | Exit Y |
11 | Hidden Exit |
12 | Other Door |
13 | Is Open |
14 | Is Locked |
16 | Weight |
17 | Size |
19 | Has Description |
20 | Is Door |
22 | Is Container |
23 | Is Operable |
24 | Is Enterable |
25 | Is 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").
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 ID | Definition |
---|---|
0 | No command selected |
1 | Start or Resume game |
2 | Close |
3 | Tick |
4 | Activate Object |
5 | Move Object |
6 | Consume |
7 | Examine |
8 | Go |
9 | Hit |
10 | Open |
11 | Operate |
12 | Speak |
13 | Babble |
14 | Target Name |
15 | Debug 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.
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.
Pops the object ID, followed by the attribute ID. Gets that object's attribute, and pushes it.
Pops the object ID, the attribute ID, and the Value. Sets the object's attribute to the value.
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.
Pushes the command ID that was passed to the script engine.
Pushes the currently selected object that was passed to the script engine.
Pushes the target object that was passed to the script engine.
Pushes the X coordinate of the drag delta that was passed to the script engine.
Pushes the Y coordinate of the drag delta that was passed to the script engine.
Reads the byte following the opcode, and pushes it onto the stack.
Reads the word following the opcode, and pushes it onto the stack.
Pops the global ID, fetches the value from the global variables array, and pushes it onto the stack.
Pops the global ID and the Value. Sets the corresponding global variable to the value.
Pops the maximum bound. Pushes a random integer from 0 up to, but not including, the maximum bound.
Peeks at the top of the stack, pushes a copy of the value onto the stack.
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.
Swaps the top 2 values on the stack.
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 the topmost value from the stack and throw it away.
Peeks at the second-top-most value on the stack and pushes a copy of it.
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.
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.
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.
Empty the stack completely.
Push the number of elements in the stack, onto the stack.
Pop B and A from the stack. Push A+B onto the stack.
Pop B and A from the stack. Push A-B onto the stack.
Pop B and A from the stack. Push A*B onto the stack.
Pop B and A from the stack. Push A/B onto the stack.
Pop B and A from the stack. Push A%B onto the stack.
Pop B and A from the stack. Push A%B and A/B onto the stack.
Pop a signed value from the stack. Push the absolute value onto the stack.
Pop a signed value from the stack. Push the negative of this value onto the stack.
Pop B and A from the stack. Push A&B onto the stack.
Pop B and A from the stack. Push A|B onto the stack.
Pop B and A from the stack. Push A^B onto the stack.
Pop a value from the stack. Push the bitwise NOT onto the stack.
Pop B and A from the stack. If neither A nor B are zero, push -1 onto the stack, otherwise push 0.
Pop B and A from the stack. If either A or B are not-zero, push -1 onto the stack, otherwise push 0.
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.
Pop a value from the stack. If the value is zero, push -1 onto the stack, otherwise push 0.
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.
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.
Pop B and A from the stack. If A is greater than B, push -1 onto the stack, otherwise push 0.
Pop B and A from the stack. If A is less than B, push -1 onto the stack, otherwise push 0.
Pop B and A from the stack. If A is equal to B, push -1 into the stack, otherwise push 0.
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.
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.
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.
Reads the signed word following the opcode, adds this word to the current opcode offset. Continues execution from there.
Reads the signed byte following the opcode, adds this byte to the current opcode offset. Continues execution from there.
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.
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.
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.
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.
Pops a priority and a function id from the stack. Pushes these onto the saved function stack to call later.
Pops a function id from the stack. Removes this function from the saved function stack.
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.
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.
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.
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.
Pops a function Id from the stack. Calls that function with the current stack.
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.
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.
Gives the current Source a queueId of 14 and pushes it onto the Object Queue. See the Queues below for more information.
Pushes an empty object with a queueId of 13 onto the Object Queue. See the Queues below for more information.
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.
Pushes an empty object with a queueId of 2 onto the TextQueue. See Queues below for more information.
Basically the same as opcode $c1 followed by opcode $c2.
Basically the same as opcode $c2, followed by opcode $c1, followed by opcode $c2 again.
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.
This is hardcoded to push the number 2 onto the stack. I'm not sure what this represents, possibly the OS identifier.
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.
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.
Pushes an empty object with a queueId of 3 onto the SoundQueue. See Queues below for more information.
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.
Pushes the current day of the week onto the stack. Sunday = 1.
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.
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.
Pushes the current engine version onto the stack. This is currently 86.
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.
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.
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.
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.
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.
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.
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.
Pops the command from the stack. Then highlights the command button which has the the popped RefCon.
Displays ALRT86 and waits for user input, the script engine is halted. This is called when you have died or otherwise lost the game.
Closes all windows, displays WIND85, draws the diploma, and prompts for your name and prints the diploma. The script engine is halted.
Pops a length of time. This is the number of ticks to sleep until continuing on. There are 60 ticks in a second.
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.
This flushes the Object Queue. See Queues below for more information.
This flushes the Sound Queue. See Queues below for more information.
This flushes the Text Queue. See Queues below for more information.
This flushes all queues. See Queues below for more information.
Pops a length of time from the stack. Inverts the main window, and sleeps for the length of time specified.
Pops an object from the stack. Loads the graphic for later use if it's not already loaded in memory.
Pops a sound Id from the stack. Loads the sound for later use if it's not already loaded in memory.
Pops B, A, and C from the stack. Multiples A and B and divides by C. Pushes the result onto the stack.
Pops an object Id from the stack. Determine the window that represents the object using the table below.
ID | Window |
---|---|
Current Room | Main Window |
-1 | Command Window |
-2 | Text Window |
-3 | Self Window |
-4 | Exit 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.
Pushes 0 if there is no sound currently playing. Pushes 1 if we're currently playing a sound.
Wait for sound to finish before continuing.
Push the result of opcode $e7 onto the stack.
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.
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.
This queue runs in order of highest queueId down to the lowest. It is mostly used for updating the objects on the screen.
queueId | Description |
---|---|
2 | Finds the window associated with the object and focuses it |
3 | Opens 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. |
4 | Close'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. |
7 | Checks the object for updating. If the object has changed, it updates the object on the screen. |
8 | Finishes the room change. Updates the window titles, and redraws the window. |
12 | Sets the attribute 6 of the old main window to 0, and sets the attribute 6 of the current player's parent to 1. |
13 | Highlights the selected exits. |
14 | Animates the object back to its original location. |
This queue runs in order of the items pushed. It is used for playing sounds.
queueId | Description |
---|---|
1 | Plays a sound in the background |
2 | Plays a sound, and waits for it to complete. |
3 | Waits for a playing sound to complete. |
This queue runs in order of the items pushed. It is used to update the text window.
queueId | Description |
---|---|
1 | Appends a number to the text window |
2 | Appends a newline to the text window |
3 | Loads the text id and appends it to the text window |
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.
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.
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.
Offset | Size | Description |
---|---|---|
$0000 | $0a | Window main |
$000a | $0a | Window text |
$0014 | $0a | Window exit |
$001e | $0a | Window commands |
$0028 | $0a | Window self |
$0032 | $0a | Window inventory |
$003c | $0a | Window diploma |
$0046 | $02 | word diplomaSignatureBottom |
$0048 | $02 | word diplomaSignatureLeft |
$004a | $02 | word diplomaSignatureWidth |
$004c | $02 | word numObjects |
$004e | $02 | word numGlobals |
$0050 | $02 | word numAttributes |
$0052 | $02 | word numGroups |
$0054 | $02 | word unknown |
$0056 | $02 | word numCommands |
$0058 | $18 | byte cmdArgCnts[24] |
$0070 | $40 | byte attrIndices[64] |
$00b0 | $80 | word attrMasks[64] |
$0130 | $40 | byte attrShifts[64] |
$0170 | $20 | String graphicsPath |
$0190 | $20 | String filterPath |
$01b0 | $20 | String textPath |
$01d0 | $20 | String objectPath |
$01f0 | $20 | String gamePath |
$0210 | $20 | String soundPath |
$0230 | $20 | String titlePath |
$0250 | $20 | String startupPath |
$0270 | $20 | String diplomaPath |
$0290 | $20 | String diplomaBWPath |
$02b0 | $20 | String volumeName |
$02d0 | $20 | word palette[16] |
$02f0 | $100 | byte paletteMap[256] |
$03f0 | $10 | String gameName |
$0400 | $30 | Menu appleMenu |
$0430 | $04 | dword unused |
$0434 | $0e | unknown |
$0442 | $02 | Filetype of saved games |
$0444 | $02 | Inventory background colors |
$0446 | $02 | word unknown (passed to Desktop) |
$0448 | $02 | word unknown (passed to Desktop) |
$044a | $0a | ColorTable windowColors |
$0454 | $10 | Control scrollbar |
$0464 | $04 | Control resize |
$0468 | $50 | String credits1 |
$04b8 | $80 | String credits2 |
$0538 | $80 | String credits3 |
$05b8 | $80 | String certificatePrompt |
$0638 | $02 | word Font ID |
$063a | $02 | word Font Size |
$063c | $28 | Pattern exitWindow background |
$0664 | $28 | Pattern exitWindow selection bg |
$068c | $02 | Number of huffman items (optional) |
$068e | $02 | Reserved (optional) |
$0690 | $bc | Huffman masks[] (optional) |
$074c | $60 | Huffman lengths[] (optional) |
$07ac | $54 | Huffman 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:
Bits | Color |
---|---|
0-3 | Blue |
4-7 | Green |
8-11 | Red |
12-15 | 0 |
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.
Offset | Size | Description |
---|---|---|
$0000 | $02 | word top |
$0002 | $02 | word left |
$0004 | $02 | word bottom |
$0006 | $02 | word right |
$0008 | $02 | word wFrameBits |
wFrameBits:
Bit | Description |
---|---|
0 | Ignored |
1 | Window zoomed |
2 | Internal usage |
3 | Controls are active when window isn't |
4 | Window has info bar |
5 | Window is visible |
6 | Window becomes active when content clicked |
7 | Window can be moved |
8 | Window has a zoom box |
9 | Size of window is flexible |
10 | Window has a grow box |
11 | Window has horiz scrollbar |
12 | Window has vert scrollbar |
13 | Window has double frame |
14 | Window has close button |
15 | Window has titlebar |
Each String contains the following data.
Offset | Size | Description |
---|---|---|
$0000 | $01 | byte 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:
Character | Description |
---|---|
* | 2 character follow, used as keyboard shortcuts |
B | Bold |
C | Character follows is used as a checkmark |
D | Disabled |
H | Word that follows is item ID |
I | Italic |
N | Same as H, but a number string follows instead |
U | Underline |
V | Divider follows this item |
X | Color-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:
Offset | Size | Description |
---|---|---|
$0000 | $01 | High-nibble is the Window Border color |
$0001 | $01 | Unknown |
$0002 | $01 | Title text color (high-nibble=inactive, low=active) |
$0003 | $01 | Inactive title background color (low nibble) |
$0004 | $01 | Active title (high-nibble=stripe color, low=background) |
$0005 | $01 | Active title style |
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.
The startup and title graphics are stored in a fairly simple format.
Offset | Size | Description |
---|---|---|
$0000 | $08 | Undocumented flags |
$0008 | $80 | Palette |
$0088 | $7d00 | Pixel Data |
There are 16 colors in the palette, each color contains a lot of useless information.
Offset | Size | Description |
---|---|---|
$0000 | $02 | Unused |
$0002 | $02 | Red |
$0004 | $02 | Green |
$0006 | $02 | Blue |
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.
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.
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:
Index | Value |
---|---|
$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.