8000 GitHub - Nutzzz/Spinnaker: A tester tool to analyze games made with Spinnaker Adventure System
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Nutzzz/Spinnaker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spinnaker Adventure System

logo logo

Used by Telarium and Windham Classics adventure games

Introduction

I wasn't able to find any technical information on the web pertaining to Spinnaker Software's "Spinnaker Adventure System" or its "Spinnaker Adventure Language" (SAL), used in creating adventure games published by Spinnaker's imprints Windham Classics and Trillium/Telarium [the name changed to the latter because of a trademark dispute]. Besides the article linked above, there is some interesting historical background that can be found at filfre.net: a series of articles beginning with this one, and a piece focusing on Byron Preiss.

screenshot screenshot

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.


NOTE: To analyze a game, the Tester Tool expects to find it in a subdirectory of "Resources\". It must be named using the game's abbreviation from the table below, followed by the port's abbreviation (e.g., "AMBAII" or "PMNIBM").

These are the games created with the Spinnaker Advanture System:

abbrev game year imprint Apple II Atari ST Commodore 64 IBM PC/PCjr Macintosh MSX*
AMZ Amazon 1984 Trillium/Telarium AII AST C64 IBM MAC MSX
DGW Dragonworld 1984 Trillium/Telarium AII C64 IBM MAC MSX
F451 Fahrenheit 451 1984 Trillium/Telarium AII AST C64 IBM MAC MSX
AMB Nice Princes in Amber 1985 Telarium AII AST C64 IBM MSX
PMN Perry Mason: The Case of the Mandarin Murder 1985 Telarium AII AST C64 IBM MSX
RDV Rendezvous with Rama 1984 Trillium/Telarium AII C64 IBM MSX
TRI Treasure Island 1985 Windham Classics AII AST C64 IBM MSX
WOZ The Wizard of Oz 1984 Windham Classics AII C64 IBM MSX

* = The MSX ports were published by Idealogic, in Spanish only. They appear to use a different engine altogether, so much of the information below does not apply.

File types

Some observations about the files used by these games: Thankfully, game 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.
<abbrev>.DIB AMB & PMN only IBM only Directory of locations with disk numbers ("a" or "b") for AMB & PMN on IBM.
*.DIB F451 & RDV only MSX only Graphics files for F451 and RDV on MSX.
DIR all but AMB,PMN all Directory of locations with disk numbers ("a" or "b").
<abbrev>.DST AMB,AMZ,PMN,TRI AST only Directory of locations with disk numbers ("a" or "b").
OUTSIDE AMB only AST only Additional directory of locations with disk nmbers ("a" or "b") for AMB on AST.
<abbrev>.EXE all IBM only The game executable. 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. 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. The Directory of locations and Vocabulary are embedded here.
TRILL all AII & C64 only The game executable?
TRILLIUM all AII & C64 only ???
*.STR all MSX only Strings for some location files have been separated into a separate file.
*.STR AMB, AMZ, & PMN MSX only Some game strings have been separated into separate files on MSX
<abbrev>.T AMB, PMN, & WOZ 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.
*.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 and sound files.
*. (no extension) all IBM only Mostly location orgraphics files. Some games use format <first initial abbrev> + <number> with no extension for graphics files.

Game strings and other data is found in the appropriate location files.

Vocabularies

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 DGW & RDV, the vocabularies are embedded in the .EXE files.

Tokenization in "Nine Princes"

To save disk space, AMB (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 can be created a dictionary with a serialized index. If a char is 0x80 or greater within any of the string lists from the Amber location files, then that represents the number of the token word--just subtract 0x80. The Tester Tool expands strings for "Nine Princes" automatically.

Picture Format

The Tester Tool permits you to export all pictures to .PNG from the IBM versions of all 8 games. You can also get a preview of an individual file with ANSI block characters. Note that the Tester's list of pictures shows files with no extension that weren't found in the location dir file (other than <abbrev>,1,2,A,B,DIR,NEWDATA,SAVED,VOLT), but there may be false positives.

For the IBM versions, SAL uses 320x200 medium-resolution CGA, which supports three 4-color palettes and 2 intensity levels; these games only use low intensity and the first 2 palettes. Note that the Atari ST and Commodore 64/128 versions use the same resolution, but with 16 color support. The Apple II versions use the 280x192 resolution, with 6 "fringed" colors.

Pictures are either placed at the top in landscape orientation, fullscreen width with (typically) 40% of the screen height, or on one side in portrait orientation, with 45% of the screen width. Note that AMZ was ported to SAL from Apple II, and it uses most of the screen for its pictures (0xA0 for both height and width, or 320x160) [plus the text is in all-caps, ungh]. My initial analysis was done on the IBM PC port, and the Tester Tool is designed for that version, but I've begun the process of recognizing other ports.

IBM PC ports

Header

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 PC color codes*
02 Unknown Lots of variance. Maybe an identifier of some kind? Differs between ports.
03 Unknown Small variance, i.e.00-10?; the game freezes after drawing is complete when values are too large, or the drawing does not complete when values are too small. Buffer size, maybe? Same values in PC and C64.
04 Height For PC and C64, typically eitherB0 (176px) = 88% height, or 50 (80px) = 40%-height
05 Width / 2 For PC and C64, typically eitherA0 (160=>320px) = 100% width, or 48 (72=>144px) = 45%-width; though this field seems to be ignored

* = For PC: 0=black, 1=dk.blue, 2=dk.green, 3=dk.cyan, 4=dk.red, 5=dk.magenta, 6=dk.yellow, 7=br.gray, 8=dk.gray, 9=br.blue, 10=br.green, 11=br.cyan, 12=br.red, 13=br.magenta, 14=br.yellow, 15=white

Pixel data

The rest of the file is pixel data. Though I don't have much experience with image formats, it seems a bit odd. It's similar to 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. 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

Decoding the Color Map

It took me an embarrassingly long time to figure out the appropriate bitwise operation to read out a base-4 bitmask, 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;

Examples

So, take an example 3 bytes: 0x1BF7C6. 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):

example

1B F 7 C6
colors 0123 15px high 7px high colors 3012
AMB\HOSPITL

So, taking the first location of "Nine Princes in Amber" as an example:

screenshot

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 = unknown
  • 50A0 = 320x80 (fullscreen width, top 40% of screen)
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.

example

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

Animations

Some of the graphic files invoke simple animations, but I haven't yet done much analysis on those.

Atari ST and Apple II ports of "Amazon" and "Nine Princes"

I have no experience with the Commodore 64/128 or Atari ST, but I figured it'd be nice to have the 16-color version of the images, and potentially some better sound files. However, for some reason, the pictures and music for these 2 games on Atari ST--and on Apple II for AMB (not sure about AMZ, since I've been unable to extract its files so far)--have been packed into container files called GRAPHPDS and MUSICPDS. The Atari GRAPHPDS appears to contain files that originally had a .GST extension, and the MUSICPDS contains files that originally had a .MST extension. PDS might refer to the "Programmers Development System" which was a hardware/software system to use an IBM PC and cross-compile to other systems, including the Commodore 64 and Atari ST.

It would be nice not to have to figure out a container format as well, and it looks like the C64 versions do not have that issue. And even though the Atari had a more flexible color system than the Commodore, based on the screenshots online it doesn't look like Telarium really leveraged it very well. I've since discovered that the other games' Atari ports don't have that issue, so perhaps I can investigate those first, but because of the above and since not every game even got an ST port, for the moment I switched my focus to the C64. (I did do a quick comparison, and they appear to be very different formats.)

Commodore 64/128 ports

OK, I've just started this analysis, but here's what I've got so far.

It's clearly a different format from IBM, but the header is similar, and there appeared to be the tantalizing similarity of three byte sequences starting at address 0x65 (after the third reference to [50A0], the resolution). Twiddling bits showed me that I was sort of correct; that there was indeed something similar going on here with a pattern of blocks with colors being placed in the first and third byte, and a size in each of the 2 nibbles of the second byte. However, here it was instead doing color fills. So the colors are also split into nibbles, with each one representing one of 16 colors in the current palette:

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:

screenshot

0x65 0x68 0x6B 0x6E
F0 71 FC 6C 11 60 1F 12 10 1F 13 0F

After experimenting, it looks like the palette is slightly different from the default C64 palette.* So that means there's probably going to be another section of the file that assigns colors. Looking at F0, I discover that Color F is light gray on both this and the default palette; same for Color 0: black; but here it looks like black is a no-op because there are no dividing lines in the top-left 4x8 pixel block, nor are there any for the next 6 blocks. And so the 7 in "num blocks A" says to use the same fill colors for seven 4x8 blocks. The next nibble, "num blocks B," is 1, saying that the third byte's colors are only going to apply to the one block. And this block occurs on both sides of the dividing line between the wall and the floor; the wall being color F again, and the floor being color C, the medium gray.

* = C64 default palette: 0=black, 1=white, 2=red, 3=cyan, 4=purple, 5=green, 6=blue, 7=yellow, 8=orange, 9=brown, 10=yellow-green, 11=rosa, 12=blue-green, 13=lt.blue, 14=zyklam [purple-blue], 15=lt.green

Looking to the next three bytes, 6 is the blue used on the bed. But why is 6 listed before C? It appears that the first byte fills from the bottom first, unlike the third byte; so perhaps what happens with the first byte in 0x65 is that the black isn't a no-op; it just fills it with black first and then gray goes on top because there's no dividing line here? Could be...

...No, it looks like it doesn't have to have a black border to be a divider, so there's something else that marks division between color boundaries.

In any case, the three-byte pattern looks like it changes again around address 0x1D6, a couple bytes before repeating the resolution (this time including the prior two bytes of the header; the ones I'm unclear on, after giving the signal 1010). So what's next? ...Or should I return to the top of the file and see if that's where the palette is being set?

Amiga port of "Perry Mason"

An Amiga magazine reviewed PMN (and is referenced by the Wikipedia article), but I can find no other indication that Amiga ports were created.

Macintosh ports

There are Mac ports for at least 3 of the games: AMZ, DGW, and F451. The pictures are higher resolution but black-and-white. I saw somewhere that RDV may also have been ported, but I haven't been able to find it (though perhaps it was referring to "Rama," an entirely separate game based on the same book). I've only been able to extract some of the files from AMZ so far, but in that case the graphics and sounds are packed into .pds files: ctxa1.pds, musa1.pds, pixa1.pds, pixa2.pds (and there's a file called names.pds that lists these filenames).

MSX ports

Then there's the Spanish-only remakes for MSX with totally redrawn art. However, it's difficult to test these; the SAL 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!

Sound format

These games feature some music and sound effects that are... serviceable. The IBM ports come with two sound formats, *.IB for the IBM PC internal speaker (monophonic) and *.JR for the IBM PCjr TI SN76489 chip (which features a 3-channel square wave generator, and a 1-channel noise generator, though it seems that SAL does not use the noise channel).

I'm still working out the format. Here's what I have so far:

Header

The first byte (at 0x00) is highly variable, maybe a buffer 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 (0x2) 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 of each beat (so larger numbers equal a slower tempo).

The fourth and fifth bytes seem to always be 18 00. Perhaps 18 is a cute way to reference IB? 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 files, positions 0x06 and 0x08 tend to be 00 and 00 or 00 and 01.

Note Lengths

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.

Note Data

For the rest of the file, starting at 0x1A, we have note data. At position 0x1A, the header is always followed by 50 00 08 40 00 80. I'm unclear what this means, except that 80 is a full stop that can be followed by a new channel. Every file ends with 80. For polyphonic files, the sequence above may occur again following an 80 stop code, and this seems to set off note data for a new channel. Sometimes there seem to be more than 3 channels, so perhaps it loops back again or sets off another section? More to investigate...

If a byte follows 00 and is between C2 and FF, this indicates an absolute pitch value that the bytes that follow are based on. 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).

hex note midi # freq (Hz)
C2 A3 57 220.00
...
D0 C4 60 261.63
...
E0 E5 76 659.46
...
F0 G#6 92 1661.2
...
FF G#7 104 3322.4

For 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 a rest is based on the note prior to the rest.

Example

AMB\AMBHORN.IB

This is a short example from "Nine Princes" that emulates 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 00C6 01 72 81 91 71 83 91 7480

The first section is the header. As mentioned above, I'm still working on deciphering this. However, I do know that the value 08 at 0x02, multiplied by 16 (i.e., 128), is the timespan in milliseconds of each beat. A beat might be thought of as an eighth note or a sixteenth note or a 32th note, so to get the tempo in beats per minute, you would multiply that value by 2 or 4 or 8 to get the length of a quarter note, then use the formula tempo = 60,000 / ms. In other words, if the beats in this file are thought of as 16th notes, then the tempo is 1/4 note=117 bpm.

The second section at 0x0B (in blue) is the 15-element array of note lengths. In this case, in decimal it is: { 1, 6, 2, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }.

The following section at 0x1A (in green) is a control sequence of some kind. Also unclear on the specifics here, but it will be repeated at the start of a new channel.

The next section at 0x21 (in red) is the note sequence. As described above, it starts with an absolute pitch. C6 corresponds with C# in the fourth octave. The next byte, 01, indicates no pitch change, for the note length in the first index of the array (in this case, one beat). 72 indicates a rise of 7 half-steps (a.k.a. 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 0x80 (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"

And finally, we get a 80 stop control code.

About

A tester tool to analyze games made with Spinnaker Adventure System

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

0