Technical data and an Extractor/Analyzer tool for Telarium and Windham Classics SAS adventure games
I wasn't able to find any technical information on the web pertaining to Spinnaker Software's "Spinnaker Adventure System" (SAS) or its "Spinnaker Adventure Language" (SAL), used in creating 8 adventure games published by Spinnaker's imprints Windham Classics and Trillium/Telarium [the name changed to the latter because of a trademark dispute]. This development company is somewhat unique as all of their games are based on books (though Shadowkeep stretched that definition). Besides the Wikipedia article linked above, there is some interesting in-depth historical background that can be found at Jimmy Maher's filfre.net: a series of articles beginning with this one about "bookware" generally, with pieces about most of these games specifically, and in some cases the authors and books on which they are based; and a later article that focuses on Byron Preiss, a central figure in the development of these games (and the co-author of the book on which the Dragonworld game was based).
Because I feel nostalgic about some of these games, but at the same time find their input text parser to be extraordinarily frustrating, I wanted to update one or more of them to a choice-based format that would enable modern players to better enjoy them. As a first step along this path, I'm documenting here my discoveries from examining the binaries for these games and comparing the various games to one another and their different ports, and for what little it might be worth, sharing the Tester Tool I created in C# to assist in my analysis. I have no particular experience in reverse engineering, but hopefully this will inspire and assist the beginning of such an effort. For instance, it'd be great to eventually have these games added to Gargoyle and/or ScummVM's Glk engine, which could provide an enhanced experience compared to DOSBox.
Important
To analyze a game, the Tester Tool expects to find its files in a subdirectory of ".\Resources\". The directory must be named using the game's abbreviation from the table below, followed by the port's abbreviation (e.g., "AMBAII" or "PMNIBM"):
Port abbreviations:
- AII = Apple II
- AST = Atari ST
- C64 = Commodore 64
- IBM = IBM PC/PCjr
- MAC = Macintosh
- MSX = MSX
Note if a file called ".\Resources.zip" or ".\Resources\Resources.zip" exists, it will automatically be extracted.
Files must be extracted from disk images before they can be analyzed. These are the tools I used:
- Apple II: CiderPress II
- Atari ST: Steem SEE emulator
- Commodore 64: DirMaster
- Macintosh: ShrinkWrap app within the Basilisk II emulator
- MSX: WinImage
For Atari, Steem SSE allows you to mount a specified directory on the host system as an emulated GEMDOS hard drive; for Mac, Basilisk II similarly allows you to add an icon to the guest that mounts specified host drive letters. In the Mac's case, the game's files are hidden from Finder, but they are accessible with the ShrinkWrap tool. In either case, files can then be copied from the mounted floppy disk to the emulated hard disk using the guest OS's GUI.
Note
There will be duplicate filenames between multiple disks, e.g., for the games with GRAPHPDS/MUSICPDS files, be sure to add an extension to keep the files separate while still allowing the Tester Tool to find them. The VOLT file is less important as it merely identifies the disk.
Note
Unlike the other ports, the Atari ST games will have a subdirectory (named with the abbreviation in the table below) for their data files (all files other than the .PRG executable), and the Tester Tool expects this layout.
These are the games created with the Spinnaker Adventure System:
| game | abbrev | year | imprint | source author | ports | manuals | reviews |
|---|---|---|---|---|---|---|---|
| Amazon | AMZ | 1984 | Trillium / Telarium | Michael Crichton | AII†, C64, IBM, MSX*, AST, MAC | [manual] | [Maher] [Dobson] [Creosote] |
| Dragonworld | DGW | 1984 | Trillium / Telarium | B. Preiss / M. Reaves | AII, C64, IBM, MSX*, MAC | [manual] | [Maher] |
| Fahrenheit 451 | F451 | 1984 | Trillium / Telarium | Ray Bradbury | AII, C64, IBM, MSX*, AST, MAC | [manual] [note] | [Maher] [Dobson] |
| Nice Princes in Amber | AMB | 1985 | Telarium | Roger Zelazny | AII, C64, IBM, MSX*, AST | [manual] | [Maher] |
| Perry Mason: The Case of the Mandarin Murder | PMN | 1985 | Telarium | Erle Stanley Gardner | AII, C64, IBM, MSX*, AST | [manual] | [Maher] [Creosote] |
| Rendezvous with Rama | RDV | 1984 | Trillium / Telarium | Arthur C. Clarke | AII, C64, IBM, MSX* | [manual] [hints] [map] | [Maher] |
| Treasure Island | TRI | 1985 | Windham Classics | Robert Louis Stevenson | AII, C64, IBM, MSX*, AST | [manual] [map] | |
| The Wizard of Oz | WOZ | 1984 | Windham Classics | L. Frank Baum | AII, C64, IBM, MSX* | [manual] |
* = The MSX versions were published by Idealogic, in Spanish only. They may use a different engine altogether, in which case much of the information below would not apply. See below.
† = The Apple II version of Amazon was not actually a SAS game; the other ports were adapted to SAS based on it. See below.
The main ports were for Apple II, Commodore 64, and IBM PC/PCjr. Later ports were made to Atari ST and Macintosh for some of the games.
There are Atari ports for 5 of the games: Amazon, Fahrenheit 451, Nine Princes, Perry Mason, and Treasure Island.
There are Mac ports for 3 of the games: Amazon, Dragonworld, and Fahrenheit 451. The pictures are similar but in a higher resolution and in black-and-white (not even grayscale).
I've seen a reference to a Mac version of Rendezvous with Rama, but I believe this is a mistake, probably referring to the 1996 Dynamix game Rama (which covers parts of the Rendezvous book as well as its sequel, Rama II).
An Amiga magazine apparently reviewed Perry Mason (which is referenced by the Wikipedia article), but I can find no other indication that any Amiga ports were created. Perhaps the magazine was reviewing the C64 port.
Finally there's the Spanish-only remakes for MSX of all 8 games published by a different company, Idealogic. The art was redrawn (or in some cases captured from photos). I'm not sure whether it's a different engine altogether.
I find it difficult to test these; the parser is hard enough to deal with when you're fluent in the language. Trying to use Google Translate as an intermediary is quite painful!
Amazon was purchased from Michael Crichton as a mostly complete game, though at that point it was based on his book, Congo. However, Crichton didn't realize that when he sold the movie rights, he had actually sold all adaptation rights. The Congo movie wouldn't actually be released until 1995.
So the game's setting was changed at the last minute from Africa to South America, and the ape that could use sign language was changed to a talking parrot. There were some oversights, like the inclusion of an incident involving hippopotamuses; hippos are not native to South America (though some escaped into the wilds from the notorious Pablo Escobar's zoo in the '90s)!
The game was written in Apple assembly, and was released in its native form, and I'm unable to dump the file contents; presumably the pictures are included in the binary. The other ports were adapted into SAS but from what I've read some features were apparently lost in translation.
The text was in all-caps (ungh!), and the initial ports to Commodore and IBM were as well. Thankfully, they fixed this for the Atari and Macintosh ports.
For completeness, I'll mention that Telarium published two other non-SAS games during its short life: [Shadowkeep](https://en.wikipedia.org/wiki/Shadowkeep_(video_game) [Maher review] (for Apple II only, and novelized by Alan Dean Foster just so they could say it was also based on a book) and Agatha Christie's [The Scoop](https://en.wikipedia.org/wiki/The_Scoop_(video_game) (for Apple II and IBM PC only).
Windham Classics published three others (for Apple II and Commodore 64 only): Zilpha Keatley Snyder's Below The Root, Lewis Carroll's Alice in Wonderland (both of which use the same Disharoon engine), and Johann David Wyss's The Swiss Family Robinson.
See this Maher article for some discussion of The Scoop and the two Disharoon games.
Robert A. Heinlein's Starman Jones and Philip José Farmer's The Great Adventure were announced by Telarium but were never released. For Windham Classics, Robin Hood was announced but never released.
Most of the games had copy protection, e.g., expecting an extra corrupted sector on the disk that wouldn't be present on a copied disk. Abandonware sites host either specially imaged disks that could replicate the corruption (e.g., Atari STX format), or a cracked executable where the check was patched out.
One file on disk 1 of each of the following games exists, but cannot be read or extracted. These files do not exist in the other ports, so they appear to be dummy files used as a copy protection measure:
- AMB: "OUTSIDE.CST"
- AMZ: "R2E.CST"
- F451: "F4LOCA"
- PMN: "GMGETUP.CST"
- TRI: "POOPDECK.CST"
For some reason, the pictures and music for AMZAST, AMBAST, and AMBAII have been packed into files called GRAPHPDS and MUSICPDS. Additionally, all three Mac ports have a similarly packed set of files (with a different header format) with .pds extensions. See below for more information about the PDS container format.
The vocabulary files list all of the words the parser understands. Note that nearly all words are truncated, but the game can be played this way, e.g. "EXAM CHAL" will examine the chalice. For Dragonworld and Rama, the vocabularies are embedded in the .EXE files.
Presumably to save disk space, Nine Princes (only) uses a tokenizer of its 256 most common words to shrink the text strings a bit. Starting at address 0x102 of AMB.TOK is a list of words, from which a dictionary can be created with a serialized index. If a char is 0x80 or greater within any of the string lists from the Nine Princes location files, then that represents the number of the token word--just subtract 0x80.
The Tester Tool expands strings for Nine Princes automatically.
Some observations about the files used by these games follow in the table below.
Most game strings and other data is found in the appropriate location files, with some general strings found in the executable. Thankfully, strings are ASCII-encoded (though AMB is partially tokenized).
| filename | games | platforms | description |
|---|---|---|---|
| <abbrev> | all | all | Strings and data used globally. |
| DEFAULTS.CST | AMB only | AST only | I'm guessing these are strings and data used globally. |
| 0 | 1 | DGW & RDV | IBM only | I'm guessing these are strings and data used globally across a specific disk. |
| A | B | AMB, F451, PMN | IBM only | I'm guessing these are strings and data used globally across a specific disk. On some ports, they might be save files. |
| A | B | C | all | MSX only | I'm guessing these identify the current disk. |
| AMBGLOB | AMB only | AII,C64,IBM,MSX | Additional strings and data used globally for AMB. |
| NEWDATA | all but TRI | AII,C64,IBM,MAC | Additional help particular to this game. |
| VOLT | all | AII,C64,IBM,MAC | Identifies the current disk. |
| SAVED | all | all | Saved game file. |
| *.DIB | F451 & RDV only | MSX only | Maybegraphics files for F451 and RDV on MSX? |
| DIR | all but AMB,PMN | all | Directory of locations with disk numbers ("a" or "b"). |
| <abbrev>.DAP | AMB & PMN only | AII only | Directory of locations with disk numbers ("a" or "b") for AMB & PMN on AII. |
| <abbrev>.DIB | AMB & PMN only | IBM only | Directory of locations with disk numbers ("a" or "b") for AMB & PMN on IBM. |
| <abbrev>.DC6 | AMB & PMN only | C64 only | Directory of locations with disk numbers ("a" or "b") for AMB & PMN on C64. |
| <abbrev>.DST | AMB,AMZ,PMN,TRI | AST only | Directory of locations with disk numbers ("a" or "b"). |
| *.GST | PMN & TRI only | AST only | Graphics files for PMN and TRI on Atari ST. |
| OUTSIDE | AMB only | AST only | Additional directory of locations with disk numbers ("a" or "b") for AMB on AST. |
| <abbrev>.EXE | all | IBM only | The game executable for IBM. Note a few game strings are found here, though most strings here are applicable to the game engine generally. |
| <abbrev>.PRG | all | AST only | The game executable for Atari ST. Note a few game strings are found here, though most strings here are applicable to the game engine generally. |
| AVENTURA.COM | all | MSX only | The game executable for MSX. The Directory of locations and Vocabulary are embedded here. |
| TRILL | all | AII & C64 only | Maybe the game executable? |
| TRILLIUM | all | AII & C64 only | An introsound file |
| *.STR | PMN | AST, C64, & IBM | Strings for some location files have been separated into a separate file. |
| *.STR | AMB, AMZ, & PMN | MSX only | Some game strings that have been separated into separate files on MSX. |
| <abbrev>.T | AMB, PMN, & WOZ | AST, C64, & IBM | Maybe a list of game functions? |
| <abbrev>.TOK | AMB only | AII,AST,C64,IBM | Token file. |
| <abbrev>.V | all but DGW,RDV | all but AST | Vocabulary file. |
| *.IB | *.JR | all | IBM only | Sound files in IBM PC and PCjr formats. |
| *.MST | PMN & TRI only | AST only | Sound files for PMN and TRI on Atari ST. |
| *.FEN | AMB only | all | Data specific to the fencing (swordfighting) events for AMB. |
| *.STR | PMN only | all | Some game strings have been separated into separate files for PMN (especially for cross-examinations?) |
| *.CST | AMB,AMZ,PMN,TRI | AST only | Location files. |
| GRAPHPDS | AMB & AMZ only | AII & AST only | Packed graphics files. |
| MUSICPDS | AMB & AMZ only | AII & AST only | Packed sound files. |
| *.PDS | all | MAC only | Packed graphics, sound, and strings/data files. |
| *. (no extension) | all | IBM only | Mostly location orgraphics files. Some games use format <first initial abbrev> + <number> with no extension for graphics files. |
| *. (no extension) | all | all but IBM & some AST | Many games use format<full abbrev> + <description> for sound files. Most non-IBM ports don't have extensions for sound files. |
The Tester Tool has a function to unpack the contents of all PDS files of the selected port type in a new "PDS\" directory under each game's directory. The extraction process runs automatically where necessary (e.g., when listing picture or sound files). A hex dump of the each file within a PDS can also be previewed.
With the 4 GRAPHPDS and 4 MUSICPDS files (one for each disk) from the Apple II port of Nine Princes: the first two bytes represent the total number of bytes in the file in little-endian (i.e., 2nd byte then 1st byte); though for some reason it seems the value given is always 5 bytes larger.
There is a 00 separator, then address 0x03 is the number of file entries, and another 00 separator.
At byte 0x05, the filenames begin, with 12 characters (8.3) per filename, padded with 00's where necessary. After a 00 separator, we have three bytes (again in little-endian), representing the starting address of that file within the PDS file. Then there's another 00 separator before the next filename.
For the GRAPHPDS/MUSICPDS files from the Atari ports of Amazon and Nine Princes, the format is the same as the Apple II except the initial three bytes are not present. The first byte is the number of file entries, then the file list starts at address 0x02; otherwise it appears to be the same.
For the pix*.pds (graphics), mus*.pds (sound), and ctx*.pds (strings and data) files from the Mac ports, the format differs from the above. The first two bytes are the starting address of the data section (little-endian). For some reason, this address is repeated at 0x2 and 0x3. Between 00 separators, there is a section between 0x05 and 0x08 that seems to always be 01 20 20 20. I'm unclear what this represents. The filename entries for Mac begin at 0x0A. Unlike the Apple and Atari ports, the filenames have a max length of only 8, and 00 and/or 20 is used as filler where necessary. There is no separator before an ending 4 bytes; unlike the GRAPHPDS/MUSICPDS format above, this value is the file's length rather than the memory address within the PDS file; it is little-endian rather than big; and for some reason, like the address at the beginning, it is 2 bytes repeated twice. The first file is always a 1-byte file called "dummy1" which I assume aids the PDS file parsing routine used for the Mac.
Graphics in SAS games are either placed at the top in landscape orientation (often multiple small picture files at once), in fullscreen width with (typically) 40% of the screen height, or sometimes on one side of the screen in portrait orientation, with 45% of the screen width, in each case leaving a good amount of space for text. Amazon is laid out differently from the others because of its development history, and tends to use most of the screen for its pictures (0xA0 for both height and width, or 160x160), which leaves much less room for text.
The Tester Tool permits you to export all pictures from all 8 games to .PNG from each port (except MSX). You can also get a preview of an individual file with Sixel, if your terminal supports it (e.g. recent versions of Windows Terminal), and if not, in ANSI block characters (though in that case they are likely to be cropped unless you shrink your font and/or enlarge your console size). Note that the Tester's list of graphics files attempts to remove any non-graphics files, but there may be false positives.
For the IBM ports, SAS uses 320x200 medium-resolution CGA, which supports three 4-color palettes and 2 intensity levels; these games (like nearly all CGA games) only use low intensity and the first 2 palettes. However, because the same pictures are used for the Commodore 64 and Apple II ports with lower resolutions, none of the pictures are greater than 160x192 pixels, so for the IBM port they have doubled the width.
The first 6 bytes are used as a header with the following layout:
| address | use | description |
|---|---|---|
| 00 | Palette | For PC CGA,00=GRY (Green/Red/Yellow) or 01=CMW (Cyan/Magenta/White) |
| 01 | Intensity/Bg | For PC CGA, 1st hex nibble is intensity (0=low; 1=bright), 2nd is background color (0-F) corresponding to IBM color codes* |
| 02 | Unknown | Lots of variance. Maybe an identifier of some kind? Differs between ports. |
| 03 | Unknown | Small variance, i.e.00-10?; Probably buffer size: the game freezes after drawing is complete when values are too large, or the drawing does not complete when values are too small. Same values in IBM and C64. |
| 04 | Height | For PC and C64, typically eitherB0 (176px) = 88% height, or 50 (80px) = 40%-height |
| 05 | Width | For PC and C64, typically eitherA0 (160px) = 100% width, or 48 (72px) = 45%-width; though this field seems to be informational in terms of drawing. Note the actual width is multiplied by 2 for CGA 320x200. |
| palette | colors |
|---|---|
00 |
0=Black, 1=Green, 2=Red, 3=Yellow |
01 |
0=Black, 1=Cyan, 2=Magenta, 3=White |
* = 0=black, 1=blue, 2=green, 3=cyan, 4=red, 5=magenta, 6=brown, 7=lt.gray, 8=dk.gray, 9=br.blue, A=br.green, B=br.cyan, C=br.red, D=br.magenta, E=yellow, F=white (However, it appears that the background color was always 0 for these games.)
The rest of the file is pixel data. Though I don't have much experience with image formats, it seems a bit odd. It reminds me of sixel (which I gather is odd enough), except this is "fourxel" and it's rotated 90 degrees. Four-pixel wide blocks are laid out top to bottom, with each block being from 0-15 pixels high (1 nibble RLE, or run-length encoding). A set of three bytes represents two of these blocks, with the first byte's color map given by the 1st nibble (hexadecimal digit) of the second byte, and the 2nd nibble of the second byte gives the height of the color map in the third byte, i.e.:
| byte1 (00-FF) | byte2 nibble1 (0-F) | byte2 nibble2 (0-F) | byte3 (00-FF) |
|---|---|---|---|
| color map | height for byte1 | height for byte3 | color map |
The following 3 bytes will place the next set of 2 blocks below the previous ones, until the height from the header 0x04 is met, then further pixels are moved back to the top and shifted right by 4 pixels. The width in header 0x05 appears to be ignored.
Color Maps: The color maps are base-4 bitmasks for the color of each of the 4 pixels. See the table below for an excerpt (the palette columns assume the background color is black):
| hex data | color map | palette 0 | palette 1 |
|---|---|---|---|
00 |
0 0 0 0 | K K K K |
K K K K |
01 |
0 0 0 1 | K K KG |
K K KC |
02 |
0 0 0 2 | K K KR |
K K KM |
03 |
0 0 0 3 | K K KY |
K K KW |
04 |
0 0 1 0 | K KGK |
K KCK |
05 |
0 0 1 1 | K KG G |
K KC C |
| ... | |||
1B |
0 1 2 3 | KGRY |
KCMW |
| ... | |||
6C |
1 2 3 0 | GRWK |
CMWK |
| ... | |||
B1 |
2 3 0 1 | RYKG |
MWKC |
| ... | |||
C6 |
3 0 1 2 | YKGR |
WKCM |
| ... | |||
FA |
3 3 2 2 | Y YR R |
W WM M |
FB |
3 3 2 3 | Y YRY |
W WMW |
FC |
3 3 3 0 | Y Y YK |
W W WK |
FD |
3 3 3 1 | Y Y YG |
W W WC |
FE |
3 3 3 2 | Y Y YR |
W W WM |
FF |
3 3 3 3 | Y Y Y Y |
W W W W |
- key: K=black, B=blue, G=green, C=cyan, R=red, M=magenta, Y=yellow, W=white
It took me an embarrassingly long time to figure out the appropriate bitwise operation to read out a base-4 bitmask (this was before AI code became popular), so if I might save you the trouble:
colorMap[0] = (byteArray >> 6) & 0x3;
colorMap[1] = (byteArray >> 4) & 0x3;
colorMap[2] = (byteArray >> 2) & 0x3;
colorMap[3] = byteArray & 0x3;
So, take an example 3 bytes: 1B F7 C6. You'll get a 4x15-pixel block above a 4x7-pixel block, with sets of 4-color stripes based on the color map. If it's palette 0, low-intensity and a black background, you'll get the following (8x zoom for clarity):
| 1B | F | 7 | C6 | |
|---|---|---|---|---|
| colors 0123 | 15px high | 7px high | colors 3012 |
So, taking the first location of Nine Princes as an example:
| address | 00 01 02 03 04 05 |
|---|---|
| data | 01 00 C9 03 50 A0 |
Looking at the header, we see:
0100= palette 1 (KCMW), low-intensity and black background.C903= unknown50A0= in form hhww, this is equivalent to WxH 160x80; note the width is doubled for IBM, so the image takes up the full screen horizontally and the top 40% of screen vertically, giving plenty of space for text below.
| offset | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F |
|---|---|
| 00 | -- -- -- -- -- -- FF FF FF FF FF FF FF 11 00 FF |
| 10 | 71 FC F0 11 01 05 11 15 54 11 51 11 13 55 FF 61 |
| ... |
Looking at the pixel data, we see that it's pretty boring at first: 61 pixels down of all white. Then 1 of all black, and seven more all white. Finally, a set of 1 pixel-high mixed black and white, and then some black and cyan until the bottom of the picture height, and back to the top of the screen (shifted by 4 pixels to the right) for the next block of white.
Note that this file doesn't include the feet shown; these are drawn with a small separate picture file (called FEET), then HOSPITL is redrawn when the player stands up.
| address | color data | height | address | height | color data | |
|---|---|---|---|---|---|---|
| 06 | FF=W W W W |
F=x15 |
08 | F=15x |
FF=W W W W |
|
| 09 | FF=W W W W |
F=x15 |
0B | F=15x |
FF=W W W W |
|
| 0C | FF=W W W W |
F=x1 |
0E | 1=1x |
00=K K K K |
|
| 0F | FF=W W W W |
7=x7 |
11 | 1=1x |
FC=W W WK |
|
| 12 | F0=W WK K |
1=x1 |
14 | 1=1x |
01=K K KC |
|
| 15 | 05=K KC C |
1=x1 |
17 | 1=1x |
15=KC C C |
|
| 18 | 54=C C CK |
1=x1 |
1A | 1=1x |
51=C CKC |
|
| 1B | 11=KCKC |
1=x1 |
1D | 3=3x |
55=C C C C |
|
| 1E | FF=W W W W |
6=x6 |
... | 1=1x |
... |
The Commodore 64 ports use 160x200 "Multicolor" resolution. At this resolution the C64's VIC-II graphics chip was capable of 16 colors at a time, however with some limtations: only 4 colors are allowed per 8x8 pixel block (3 colors plus a global background). That said, I don't think there's ever more than 8 colors total in any of these pictures.
The developers had to use a thin blocky font with the C64 because of the limited horizontal resolution.
At this point, I've managed to get almost all of the files decoded. There are still a few that are bugged partway through.
It's a different format from IBM, but the header has similarities, though there are similar sets of three byte sequences. However, here the file is divided into 4 different sections:
- Section 1: Header
- Section 2: Tertiary Color
- Section 3: Primary and Secondary Colors
- Section 4: Bitmasks
The Sections are delimited by the two byte picture dimensions (e.g., 50 80 in the "hosptl" example).
Like the IBM picture data, these are also divided into run-length encoded 3-byte sequences. Section 2 and 3 provide color data for the picture, divided into 4x8 blocks, laid out vertically, with the primary two colors in Section 3 and a tertiary color in Section 2.
Section 3's three-byte sequence specifies two colors in the first and third bytes (each nibble representing a color from the C64 palette*), two run-lengths in the second byte (nibble 1 referencing byte 1's colors and nibble 2 referencing byte 3's colors). Each run-length is over a number of 4x8 blocks.
Section 2 is the same as Section 3, but the low nibble of byte 2 is the third color for some number of 4x8 blocks, and the high nibble is always zero.
Section 4's three-byte sequence, like the IBM version above, specifies a 4x1 bitmask in the first and third bytes, and two run-lengths in the second byte, nibble 1 referencing byte 1's bitmask and nibble 2 referencing byte 3's bitmask.
* = C64 palette: 0=black, 1=white, 2=red, 3=cyan, 4=purple, 5=green, 6=blue, 7=yellow, 8=orange, 9=brown, A=lt.red, B=dk.gray, C=md.gray, D=lt.green, E=lt.blue, F=lt.gray
It does seem that 2 (red) is [sometimes?] remapped as A (light red) and 6 (blue) is remapped as E (light blue).
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| color A2 | color A1 | num blocks A | num blocks B | color B1 | color B2 |
So, returning to Nine Princes for our example:
At address 0x65 (after the third reference to [50A0], the resolution), is an example of Section 3:
| 0x65 | 0x68 | 0x6B | 0x6E |
|---|---|---|---|
F0 71 FC |
6C 11 60 |
1F 12 10 |
1F 13 0F |
Looking at F0, Color F (15) is light gray and Color 0 is black. The first color is used as the background color of a number of 4x8-pixel blocks provided by the first nibble of the second byte (here 7), from top to bottom. The second color is the foreground color for the same set of blocks (which will be used by another part of the file). In other words, at this stage we fill a 4x56 square with light gray, and remember black to be used later. The next nibble of the second byte, "num blocks B", is 1, saying that the third byte's two colors are going to apply to just one 4x8-pixel block. Looking at the "hosptl" picture, this block contains 3 colors in the division between wall and floor; the wall being color F (light gray), the floor being color C (medium gray), and the dividing line being color 0 (black). Then looking to the next three bytes, 6 is blue which is used for the bed (though it gets remapped to A, light blue), and so it goes.
The availability of black to be used for the dividing line mentioned above comes from Section 2. So if you check Section 2 above at address 0x0A:
| 0x0A | 0x0D | 0x10 | 0x13 |
|---|---|---|---|
00 FF 00 |
00 F2 00 |
0C 19 00 |
06 34 00 |
You'll see that in this case 0 is the tertiary color for the first 47 4x8 blocks (F + F + F + 2 = 15 + 15 + 15 + 2), which includes the block in question.
The Apple II ports use 280x192 "HIRES" resolution, with 6 colors. The Apple II is an odd duck; it's actually monochrome, but the pixel layout in conjunction with NTSC color burst signals results in the colors being displayed.
I've mostly got this one figured out. There's a couple of files that are skewed, and many from Nine Princes are cut off at the end (though in that case it could be the PDS extraction is to blame).
The initial header has 7 bytes (one additional compared to PC). I'm currently not sure of the purpose of these other than the width and height at address 0x05 and 0x06.
After this, here again are the three-byte sequences, but now instead of four pixels with vertical runlengths as in the IBM CGA files, it's seven subpixels. This actually makes sense because of the strange way Apple II graphics are set in memory. The bits in the first and third bytes are split into sequences, little-endian order. Seven bits are read right-to-left and placed horizontally left-to-right onscreen, with 0 representing "off" and 1 being "on". The first (leftmost) bit sets the palette (which is actually the color burst orientation). So two "off" subpixels result in a black pixel, and two "on" subpixels result in a white one. When an odd "on" subpixel adjoins an even "off" one, it results in either a green (with 0 palette) or orange (with 1 palette) pixel. Finally, when an odd "on" subpixel adjoins an even "off" one, we get a purple (0) or blue (1) pixel.
In other words, if the 8 bits of a byte in binary are laid out like this: PQRSTUVW then the purpose of each bit is: palette = P subpixel layout (odd) = WV UT SR Q subpixel layout (even) = W VU TS RQ
To determine color, couples of adjoining odd and even bits (in conjunction with the palette) can be examined. Assuming this is an odd column, then for the rightmost subpixel (Q in the pattern above), you can't determine the appropriate color until you've gone down the screen and come back up to the same y-value, 7 subpixels to the right, to then combine it with the leftmost subpixel (W) of that 7-bit pattern. This wouldn't have mattered as much for the Apple II since it was all an artifact of the hardware, but for the purposes of emulating the result that means we have to process the contents in two passes, where the first pass generates a monochrome subpixel map and the second pass examines subpixel couples to produce the final color pixel map.
Palette Colors:
| palette | 00 |
01 |
10 |
11 |
|---|---|---|---|---|
0 |
0=Black | 1=Green | 2=Purple | 3=White |
1 |
4=Black | 5=Orange | 6=Blue | 7=White |
Example:
| hex | binary | palette | subpixel bits | color sequence |
|---|---|---|---|---|
67 |
0b0110_0111 |
0 |
odd:11 10 01 1 |
W P G ?? |
even:1 11 00 11 |
?? W K W |
So 00 is black and 11 is white, regardless of palette. 10 (assuming an odd column and palette 0) is purple, and 01 might be green, but since these are 7-subpixel patterns, every other vertical stripe changes which bit is even and which is odd. Worse, if the palette changes then at the interface where the palettes differ, undesired artifacts occur. There is an automatic half-pixel shift to prevent severe glitches in both vertical and horizontal directions, but the color fringes that occur either need to be suffered, or the artist/programmer would have needed to carefully design their art to work around the issues. This includes needing to be aware of using the "correct" black or white (ie., 0 vs. 4, or 3 vs. 7, as numbered in the palette map above). What fun that all must have been!
When converting to PNG, I'm not currently trying to produce an image with any real fidelity to Apple II hardware. I could, following the behavior of some modern emulators, at least duplicate the white banding at horizontal palette interfaces. However, this would require doubling the horizontal resolution to accommodate.
The Atari ports use their low resolution 320x200 mode, and like IBM most pictures are multiplied by 2 across the horizontal to fill the screen.
Though the ST had a more flexible color system than the Commodore (e.g., it could display 16 colors from 512 possible without any of the 8x8 block limitations), Telarium didn't really leverage it very well, and just stuck to a max of 8 colors. They also relied solely on the default system palette.*
* = Atari ST default palette: 0=white, 1=black, 2=red, 3=green, 4=blue, 5=cyan, 6=yellow, 7=magenta, 8=lt.Gray, 9=dk.gray, A=dk.red, B=dk.green, C=dk.blue, D=dk.cyan, E=Brown, F=dk.magenta
Looking at the header, the first two bytes are the file size. Address 0x03 and 0x05 have the resolution; instead of hh ww as all of the other files so far, the resolution here is width first, and with 00 separators (or perhaps the spread was with the idea of supporting higher resolutions).
Following the resolution, there is a sequence that is common to most files:
00 0F 00 00 0F 00 0D 0B 0F 0B 09 0B 0F 0D 00 00 0B 0F 0F 0D 08 0F 06 00 09 08 09 0B 0E 0D 00 0F 0F 0F 02 03 01 0E 09 0E 0C 08 0A 0C 03 0E
This is the 16-color palette. The Treasure Island palette is slightly different, and Amazon is quite different and even differs between files.
The high nibble is always zero, but the low nibble varies from 0-F rather than 0-7 as I would think it should for 512 possible colors. In any case, after some experimentation, it looks like it's not laid out in RGB order, i.e. 'R0 G0 B0 R1 G1 B1 R2 G2 B2...' but rather it appears to be "ramped", so first occurs each red, then each green, then each blue, i.e. 'R0 R1 R2... G0 G1 G2... B0 B1 B2...' So you'll need to parse the whole thing and collate it.
So the following is the most common palette, after collating the values. The Rgb24 equivalent is found by multiplying by 17 (so e.g., 09 becomes 99):
| hex | 00 00 00 |
02 00 0D |
00 0B 0F |
0F 0F 0F |
00 0F 02 |
00 0D 03 |
0F 07 00 |
00 0F 0E |
0D 06 09 |
0B 00 0E |
0F 09 0C |
0B 08 08 |
09 09 0A |
0B 0B 0C |
0F 0E 03 |
0D 0D 0E |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Rgb24 | #000000 | #2200DD | #00BBFF | #FFFFFF | #00FF22 | #00DD33 | #FF7700 | #00FFEE | #DD6699 | #BB00EE | #FF99CC | #BB8888 | #9999AA | #BBBBCC | #FFEE33 | #DDDDEE |
| Name | 0=Black | 1=Blue | 2=Dk.Cyan | 3=White | 4=Green | 5=Dk.Green | 6=Red | 7=Cyan | 8=Dk.Red | 9=Purple | A=Pink | B=Brown | C=Dk.Gray | D=Md.Gray | E=Yellow | F=Lt.Gray |
The next section begins at address 0x36, and is split into up to 4 bitplanes. The output is in 4x2 blocks, and unlike the prior formats these are laid out horizontally. 0x00 and 0xFF, though they are also valid values (see below), use the following byte as a run-length.
The first nibble is the top 4 pixels, and the second nibble is the bottom 4. The layout is very simply obtained by converting the nibble to binary.
Examples:
| nibble | binary | pixel mockup |
|---|---|---|
0x0 |
0 0 0 0 |
____ |
0x1 |
0 0 01 |
___█ |
0x6 |
01 10 |
██ |
0xC |
1 10 0 |
██__ |
0xF |
1 1 1 1 |
████ |
So putting the nibbles together, e.g., 0x88 would be two pixels on the left side of the 4x2 block.
Likewise, 0x00 is an empty 4x2 block of pixels, and 0xFF is a completely filled block, though as mentioned above, these values (only) expect a run-length to follow.
If there are two or more 0x00s in a row, then each byte couple adds 256 to the next run-length (i.e., 00 00 00 00 00 9B becomes run-length 0x29B).
Finally, as to determining colors, the layout of the pixels on each bitplane gives a component of a bitmap for each pixel to derive the palette index. We build a binary number from right to left. So an "on" pixel in bitplane 0 adds 0b0001 (1) to the palette index, an "on" in bitplane 1 adds 0b0010 (2) to the palette index, one in bitplane 2 adds 0b0100 (4) to the palette index, and one in bitplane 3 adds 0b1000 (8) to the palette index. In this way, we can specify each of the 16 colors in the palette.
Palette Index Example:
| bitplane | value |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 0 |
| 3 | 1 |
This results in a bitmap value of 0b_1010 (10) for the corresponding pixel, which, if using the most common of the custom palettes shown above, will be pink.
The Mac ports use a similar art style to the other ports, but in a higher resolution--though the full screen resolution is 512x342, the game only uses 480x300--and in black-and-white only (not even grayscale), as that's all that the first Macs supported.
For all three games, the first two bytes are the width in pixels, and the next two are the height.
For the Fahrenheit 451 images, Spinnaker just used a standard Macintosh RLE format called PackBits. The Tester Tool extracts these files too, but there are several implementations available elsewhere.
As for Amazon and Dragonworld, they use a custom format:
Address 0x04 is always 00, and address 0x05 is either 00 or 01; when it is 01, the picture is rotated by 270° (i.e., 90° counter-clockwise), which I'm guessing is about capitalizing on vertical run-length encoding when horizontal encoding would be less effective.
The range from address 0x06 to 0x15 is a bitmap lookup table. There are two 8-byte sections, but the sections are reversed when referenced, i.e., index 2 at address 0x07 is referenced by A, and index 10 at address 0x11 is 2. You'll find that the unused values at indices 0 and 8 are always 00.
The picture data begins at address 0x16, and every nibble needs to be considered separately. If a nibble is less than 8 (including 0), then the following nibble is a run-length. If the nibble is either 0 or 8, then the next two nibbles (in the case of 0, the next two after the run-length) are used as a literal bitmap. If the nibble was 1-7 (followed by a run-length) or 9-F (without a run-length), then that is the index of a byte in the lookup table.
And that's it! You now have a series of bytes where each is an 8-pixel wide stride, laid out horizontally (or vertically if rotated). This being a Mac, 0 is white and 1 is black.
These "ports" were released by a different company, and the art was redrawn with a new art style. I'm not clear whether these are actually SAS games, or adaptations in a different engine.
These games feature some music and sound effects that are... serviceable.
I've got the format figured out for the most part, but there are some bugs to work out. For what it's worth, the Tester Tool permits you to export all audio files to .MID for all games (except MSX ports) as far as my current understanding goes (and will automatically extract files from PDS containers first), but the pitch is incorrect in some cases.
The tool will also preview sound files for individual games, but in that case it will only play one channel at a time.
So here's what I've deciphered so far:
The IBM ports come with two sound file formats, *.IB for the IBM PC speaker (monophonic) and *.JR for the IBM PCjr TI SN76489 chip (polyphonic, featuring a 3-channel square wave generator, and a 1-channel white noise generator, though I'm pretty sure the noise channel is not used on the IBM ports).
The first byte (at 0x00) is highly variable; it seems to be a buffer size, as I got it to play part of a prior sound file after increasing the size. The second byte (0x01) has a very small range (00-02 I think). The first often varies between IB and JR formats, and the second sometimes does as well.
The third (0x02) does not appear to vary between formats. It represents the timespan of the shortest beat length. The range is also small (01-08). I believe you simply multiply the number by 16 to get the number of milliseconds for each beat (so larger numbers equal a slower tempo).
0x03 and 0x04 seem to always be 18 00.
For monophonic files (which includes many of the *.JR files which are duplicates of the *.IB ones), the next 6 bytes are all 00s, whereas for polyphonic *.JR files, 0x05 and 0x07 often have a wide variance, where positions 0x06 and 0x08 have a very small range (00-03), though usually both are 00. Based on my limited experiments, 0x05 and 0x07 seem to adjust both octave and starting position of the track, for the second and third channel respectively; where 0x06 and 0x08 might be adjustments to the buffer size.
0x09 and 0x10 are always 00.
The 15 bytes between positions 0x0B and 0x19 comprise a new section that specifies an array of note lengths that are used in the section below.
For the rest of the file, starting at 0x1A, we have note data. The control code at the beginning is always 50 00 08 40 00 80 on IBM. For polyphonic files, a similar sequence starting with 50 will start a new channel after an 80 stop control code. The new channel begins at the beginning of the song and plays at the same time as the prior channel. I'm unclear on some of the specifics of this sequence, but the 08 40 is used for the PC speaker or for square waves on the PCjr. Other ports sometimes have different waveform values. Also, the 80 at the end doesn't indicate a stop (or even a start) control code as it would during the note data proper; it can differ in some ports.
The first byte that follows the new channel sequence could indicate one or more rests (see next section below), but before the first audible note there should be a 00 followed by a byte between C2 and FF. This indicates an absolute pitch value. This may seem a fairly narrow range of values to represent a ~2500 Hz range, except that since only musical notes will be specified, this actually covers more than 4 octaves of a chromatic scale (and the frequency curve is exponential anyway). See the chart below for some examples. It looks like you can set it to a lower pitch (below A3) with values below C2 (though it will go no lower than C at octave zero). From my experiments, it seems that notes don't cross over the C2 line, i.e., pitch changes (see next section below) may end up going down instead of up. The same is not true for pitch changes that go above FF; those seem to be allowed.
| hex | note | midi # | freq (Hz) |
|---|---|---|---|
C2 |
A2 | 45 | 110.00 |
| ... | |||
D1 |
C4 | 60 | 261.63 |
| ... | |||
E0 |
Eb5 | 75 | 622.25 |
| ... | |||
F0 |
G6 | 91 | 1568.0 |
| ... | |||
FF |
Bb7 | 106 | 3729.3 |
For the following note values, each byte represents one note or rest. The first nibble is the relative note pitch compared to the prior pitch, with 0 indicating the same note, 1-7 indicating the number of notes above, and 9-F indicating the number of notes below, with F=-1, E=-2, D=-3, etc. to 9=-7.
If the first nibble is 8, then it is a rest (no sound for the same duration as if it was a regular note)--except for 80, which remember is the stop control code. A rest does not change the pitch; the pitch of the note following one or more rests is based on the note prior to the rests.
A 00 within the note values seems to indicate a key change, though the value is still relative to the prior note. If the first nibble is 0-7, then the key moves up; I believe the first nibble is the number of octaves; the second is definitely the number of semitones (a.k.a. half-steps). If the first nibble is 9-F, then the key moves down. The second nibble should be subtracted from 0x10 to get the number of semitones to lower, i.e., F7 means 0x10(16)-0x07(7)=9.
I'm not sure what 2 00s indicate...
This is a short example from Nine Princes that mimics the sound of a hunting horn.
| offset | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F |
|---|---|
| 000 | 2B 00 08 18 00 00 00 00 00 00 0001 06 02 10 01 |
| 010 | 01 01 01 01 01 01 01 01 01 0150 00 08 40 00 80 |
| 020 | 00 C601 72 81 91 71 83 91 7480 |
The first section is the header. I do know that the value 08 at 0x02, multiplied by 16 (here, 128), represents the timespan in milliseconds per beat. A beat might be thought of as an eighth note (♪) or a sixteenth note (𝅘𝅥𝅯) or even a 32nd note, depending on the tempo. You should be able to calculate a tempo in standard quarter-note (♩) beats per minute by first multiplying the length in ms by 2 (for 8th note), 4 (for 16th note), or 8 (for 32nd note) to get the length in ms of a quarter note, then use the formula "bpm = 60,000 ÷ (beat length × note length)". In other words, if the beats in this file are thought of as 16th notes, then the tempo is 60,000 ÷ (128 × 4), i.e.: ♩=117 bpm (where 108-120 is considered moderate).
The second section at 0x0B is the 15-element array of note lengths. In this case, in decimal the array is: { 1, 6, 2, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }.
The next section at 0x1A is the control sequence at the start of a channel, which on IBM will always be the same. The same sequence will be repeated at the start of a new channel for polyphonic files.
C6 (at 0x21) is the absolute pitch at the start of the (only) channel. It corresponds with C# in the fourth octave. The following section represents notes. 01 indicates no pitch change from the prior value, for the note length in the first index of the array (in this case, one beat). 72 indicates a rise of 7 semitones (i.e., C#->D->D#->E->F->F#->G->G#) up to G#, still in the fourth octave, for the length provided in the second index of the array (six beats). The next byte, 81, since the first nibble is 0x8 and it's not 80 (the stop control code), then it's a rest for the length in the first index of the array (one beat). 91 indicates a fall by 7 half-steps, returning us to C#4, for one beat. In the end, the entirety of the audio is:
C#4 x1, G#4 x6, rest x1, C#4 x1, G#4 x1, rest x2, C#4 x1, G#4 x16
It sounds like: "Da-doooo, da-do da-dooooooo!" Listen to the MIDI conversion here.
And finally, we get the 80 stop control code.
The three models prior to the IIGS had a very basic speaker. Add-in music cards were available, but SAS only supports this speaker.
Because of this, the sound files are very similar to the .IB versions. There are three extra bytes at the start of the file, however, so the note data doesn't start until address 0x23.
All four models prior to the STe had a Yamaha YM2149, which apparently produces "similar results" to the chip in the PCjr.
Given that, perhaps it should not be surprising that the sound files are very similar to the PCjr versions; some are actually identical.
For AMB and PMN (the last 2 SAS games to be released), the Atari control code format is a bit different from the other Atari ports (though note AMB has its music files packed into a MUSICPDS file). The first channel uses 8 bytes instead of 6, but later channels only use 2 bytes. The first channel often begins with 50 00 00 00 38 00. The following two bytes always start with 60, which is usually followed by 0D, but is sometimes 0E, 0F, or 10. This seems to be the waveform type: 0D is a square wave, but I haven't yet figured out the others. The second and third channel codes consist of only 2 bytes, again 60 and one of 0D through 10.
The Commodore 64 files I've analyzed are also very similar to the PCjr ones. The C64's MOS 6581 SID is 3-channel, though it is much more flexible with the sounds that it can output (i.e., each channel can use noise generation or 4 different waveforms simultaneously, filtration and ADSR envelope modification).
Note I have also heard at least one instance of the noise channel being used by the C64 in an emulator.
There are an additional two bytes at the beginning of a file that are always 20 41 in the ones that I've looked at so far.
The control code at the start of a channel is a bit different. It's still six bytes starting with 50 00, but the next 4 control the waveform (and ADSR envelope?). Initially I thought the 80 in the sixth byte for IBM and Atari meant that it was both start and stop, but since it varies here it probably doesn't mean start at all. The third byte seems to always be 08 or 0F. The latter seems to add a duplicate waveform (sawtooth maybe?) with the same pitch on top, though it only seems to work if it is followed by 40 (square wave); as the fourth byte is the waveform type: it seems that 10 is triangle and 20 is sawtooth; I believe 80 is white noise.
C64 also has a final byte after the last 80 that varies: I've seen e.g., 00, 01, 02, 81, and FF.
















