2023-02-06

World Builder Formats

 The following is documentation that I've created as a result of reverse engineering the various file formats used by World Builder games.

NOTE: All types are big-endian unless otherwise specified.

Overview

It is assumed that you can read HFS disk images. As a part of being able to read HFS disk images, you can also parse a file's resource fork.   All of this is well-documented on the internet.  The formats for standard resources (such as DLOG or MENU) are also well-documented and outside the scope of this blog post.

Main Resource Fork

The main executable contains a resource fork which holds nearly all of the assets and UI for the game. It can be identified by the filetype APPL and the creator WEDT.

Common Structures

The following structures are commonly used by the various data formats:

struct Rect {
    int16 top;     // 00
    int16 left;    // 02
    int16 bottom;  // 04
    int16 right;   // 06
}
struct String {
    uint8 length;  // 00
    uint8 data[];  // 01
}

ACHR

This contains the graphics and information on various characters in the game.  This includes the main player, or players if there are more than one.

All strings, except for initial_scene can be blank.

struct ACHR {
    uint16   offset_to_bounds;   // 00
    Graphics graphics[];         // 02
    Rect     bounds;             // offset_to_bounds
    uint8    physical_strength;  // +08
    uint8    physical_hp;        // +09
    uint8    physical_armor;     // +0a
    uint8    physical_accuracy;  // +0b
    uint8    spiritual_strength; // +0c
    uint8    spiritual_hp;       // +0d
    uint8    magic_resistance;   // +0e
    uint8    spiritual_accuracy; // +0f
    uint8    running_speed;      // +10
    uint8    reject_offers;      // +11
    uint8    follows_opponent;   // +12
    uint8    unknown;            // +13
    uint32   unknown;            // +14
    uint8    weapon1_damage;     // +18
    uint8    weapon2_damage;     // +19
    uint8    unknown;            // +1a
    uint8    is_player;          // +1b
    uint8    carry_amount;       // +1c
    uint8    return_to;          // +1d
    uint8    winning_weapon;     // +1e
    uint8    winning_magic;      // +1f
    uint8    winning_run;        // +20
    uint8    winning_offer;      // +21
    uint8    losing_weapon;      // +22
    uint8    losing_magic;       // +23
    uint8    losing_run;         // +24
    uint8    losing_offer;       // +25
    uint8    gender;             // +26
    uint8    proper_noun;        // +27
    String   initial_scene;      // +28
    String   weapon1_name;
    String   weapon1_verb;
    String   weapon2_name;
    String   weapon2_verb;
    String   initial_comment;
    String   score_hit;
    String   receive_hit;
    String   make_offer;
    String   reject_offer;
    String   accept_offer;
    String   die;
    String   initial_sound;
    String   score_hit_sound;
    String   receive_hit_sound;
    String   die_sound;
    String   weapon1_sound;
    String   weapon2_sound;
}

offset_to_bounds
    Byte offset to beginning of bounds rectangle. If there are no graphics, this will be 2.
graphics
    Visual representation of this character.  The format is documented in the Graphics section.
bounds
    The bounding box for the character's graphics.
physical_strength
    Determines damage done with physical weapons.
physical_hp
    The character's hit points.
physical_armor
    Reduces damage taken from physical attacks.
physical_accuracy
    Chances of missing a physical attack.
spiritual_strength
    Determines damage done with magic
spiritual_hp
    The character's mental hit points.
magic_resistance
    Reduces damage taken from magical attacks.
spiritual_accuracy
    Chances of missing a magical attack.
running_speed
    Used to check if you can flee an enemy, or chase one down.
reject_offers
    Maximum price of offers to reject.  Offers above this will be accepted.
follows_opponent
    Chance of following you if you run.
weapon1_damage
    Damage caused by first innate weapon (punches or claws, etc).
weapon2_damage
    Damage caused by second innate weapon (kicks or fire breath, etc)
is_player
    There can be multiple players.  In that case, the game will chose a random character marked with is_player as the main player character.
    0: enemy
    1: player
carry_amount
    Number of items you can have in your inventory.
return_to
    Scene to teleport the character to when it dies.
    0: STORAGE@
    1: RANDOM@
    otherwise: use initial_scene
winning_weapon
    Odds of using a weapon if the mob has more HP than you.
winning_magic
    Odds of using magic when winning.
winning_run
    Odds of fleeing when winning
winning_offer
    Odds of making an offer when winning.
losing_weapon
    Odds of using a weapon if mob has less HP than you.
losing_magic
    Odds of using magic when losing
losing_run
    Odds of fleeing when losing
losing_offer
    Odds of making an offer when losing.
gender
    0: male
    1: female
    2: it
proper_noun
    Whether or not a character's name is a proper noun.  "A robot attacks" vs "Robot attacks".
initial_scene
    The name of the initial scene for this character. Can also be STORAGE@ or RANDOM@.
    If so, then return_to should be set accordingly.
weapon1_name
    The name of the first innate weapon. (Claws, fire, etc).
weapon1_verb
    The verb to use for that weapon. ("breathe" fire, etc.)
weapon2_name
    The name of the second innate weapon.
weapon2_verb
    The verb to use for that weapon.
initial_comment
    Message to print when mob first appears.
score_hit
    Message to print when mob scores a hit.
receive_hit
    Message to print when mob gets hit.
make_offer
    Message to print when mob makes an offer.
reject_offer
    Message to print when mob rejects your offer.
accept_offer
    Message to print when mob accepts your offer.
die
    Message to print when mob dies.
initial_sound
    Sound to play when mob first appears.
score_hit_sound
    Sound to play when mob scores a hit
receive_hit_sound
    Sound to play when mob gets hit.
die_sound
    Sound to play when mob dies
weapon1_sound
    Sound to play when mob uses first innate weapon.
weapon2_sound
    Sound to play when mob uses second innate weapon.

AOBJ

This contains the graphics and information for various objects in the game.

struct AOBJ {
    uint16   offset_to_bounds;  // 00
    Graphics graphics[];        // 02
    Rect     bounds;            // offset_to_bounds
    uint8    plural;            // +08
    uint32   unknown;           // +09
    uint8    accuracy;          // +0d
    uint8    value;             // +0e
    uint8    type;              // +0f
    uint8    damage;            // +10
    uint8    effect;            // +11
    int16    uses;              // +12
    uint8    return_to;         // +14
    uint8    unknown;           // +15
    String   owner;             // +16
    String   pickup_message;
    String   operative_verb;
    String   failure_message;
    String   successful_use;
    String   operative_sound;
}
enum Types {
    unknown         = 0,
    regular_weapon  = 1,
    thrown_weapon   = 2,
    magical_object  = 3,
    helmet          = 4,
    shield          = 5,
    chest_armor     = 6,
    spiritual_armor = 7,
    mobile_object   = 8,
    immobile_object = 9,
}
enum Effects {
    cause_physical_damage  = 0,
    cause_spiritual_damage = 1,
    cause_both_damage      = 2,
    heal_physical_damage   = 3,
    heal_spiritual_damage  = 4,
    heal_both_damage       = 5,
    freeze_opponent        = 6,
}

offset_to_bounds
    Byte offset to beginning of bounds rectangle. If there are no graphics, this will be 2.
graphics
    Visual representation of this object. The format is described in the Graphics section.
bounds
    Bounding box for the object's graphics.
plural
    Whether or not this object is plural.  "You see an arrow" vs "You see some arrows".
accuracy
    Accuracy modifier if this is a weapon.
value
    Price of this object, for making offers.
type
    Type of object, Immobile_objects cannot be moved, they're used to decorate a scene.
damage
    If this is a weapon, this is the damage it causes. If this is armor, then this is the amount of protection in provides.  If this is a magical object, then this is the spell armor.
effect
    If this is a magical device, this determines what it does.
uses
    Number of times this object can be used before it breaks. -1 : indestructible.
return_to
    Where to respawn this object when it breaks.
    0: STORAGE@
    otherwise: Spawn in a random scene.
owner
    Initial owner of this object. Might be the name of a scene or a character. Can also be STORAGE@ or RANDOM@. If random, that means a random scene, not a random character.
pickup_message
    Message to print if the item is picked up
operative_verb
    Verb to use for this object. "Eat" a cake, for example.
failure_message
    Message to print if item breaks.
successful_use
    Message to print if item is used successfully.
operative_sound
    Sound to play when item is used.

ASCN

This contains the graphics and information for every room or "scene" in the game. It also determines the size and location of the graphics window.

struct ASCN {
    uint16   offset_to_bounds;    // 00
    Graphics graphics[];          // 02
    Rect     bounds;              // offset_to_bounds
    uint16   map_y;               // +08
    uint16   map_x;               // +0a
    uint8    blocked_north;       // +0c
    uint8    blocked_south;       // +0d
    uint8    blocked_east;        // +0e
    uint8    blocked_west;        // +0f
    uint16   frequency;           // +10
    uint16   loop_type;           // +12
    String   north_text;          // +14
    String   south_text;
    String   east_text;
    String   west_text;
    String   ambient_sound;
}

offset_to_bounds
    Byte offset to beginning of bounding box. If there are no graphics, this will be 2.
graphics
    Visual representation of this scene. The format is documented in the Graphics section.
bounds
    Bounding box for the main graphics window.
map_y
    Y coordinate (0-49) of this scene in the world map.
map_x
    X coordinate (0-49) of this scene in the world map.
blocked_north
    Whether or not the north exit is blocked
blocked_south
    Whether or not the south exit is blocked.
blocked_east
    Whether or not the east exit is blocked.
blocked_west
    Whether or not the west exit is blocked.
frequency
    How often to play the ambient sound while in this scene. The actual meaning of the value depends on loop_type.
loop_type
    How to loop the ambient sound. 0 means play the sound frequency times per hour, with the first time being when you enter the room. Otherwise, play the ambient sound randomly.  In that case, there is a frequency out of 3,000 chance of playing for every second you remain in the room.
north_text
    Message to print if you try to go north while it is blocked.
south_text
    Message to print if you try to go south while it is blocked.
east_text
    Message to print if you try to go east while it is blocked.
west_text
    Message to print if you try to go west while it is blocked.
ambient_sound
    Sound to play as ambient noise.  See frequency and loop_type for how to play this.

ATXT

This contains the intro text for a room or scene, as well as information on the text window's location and font.  There is one ATXT for every ASCN.

struct ATXT {
    Rect    bounds;  // 00
    uint16  fontid;  // 08
    uint16  size;    // 0a
    char    text[];  // 0c
}

bounds
    The bounding box for the text window
fontid
    The Macintosh font ID for the font to use
size
    Font size (in points) to use
text
    Text to show. This just runs until the end of the resource.

ASND

This contains a single digitized sound.

struct ASND {
    uint16  channels;    // 00
    uint16  loops;       // 02
    int8    deltas[16];  // 04
    uint8   data[];      // 14
}

channels
    I believe this is the number of channels. Every sound I've found is always 1 (mono) however.
loops
    The number of times to loop this sound.
deltas
    A 16-byte array of deltas to use when parsing the sample data.
data
    Each byte represents 2 samples.  The high nibble is the first sample, the low nibble is the second. You use each nibble to look up a delta in the deltas array, and add it to the current sample value. The resulting byte is the next sample.  The sample value starts at $80.

The final sample is an 8-bit unsigned PCM array.  The sound is always played back at 22.2545 kHz, but stored at half that.  The way the engine compensates for this is to store last + delta in buffer[i + 1] and last + delta / 2 in buffer[i], essentially interpolating the audio.

ACOD / GCOD

This contains the code for the scene or global context.  There is an ACOD for every ASCN in the game, and a single GCOD resource.  The GCOD resource is only run when the ACOD resource for the scene exits without hitting an EXIT opcode.

These code resources are run whenever the user does something, like clicking on an object or typing a command.  Whenever a user changes scenes, a fake LOOK command is issued.

struct ACOD {
    Rect    bounds;  // 00
    uint16  fontid;  // 02
    uint16  size;    // 0a
    char    code[];  // 0c
}

bounds
    Bounding box for the script window.  Only used in the world editor, since the script window isn't visible in game.
fontid
    Macintosh font ID for the script window.
size
    Font size (in points) for the script window.
code
    The code to run for this script, see the section below on Code format.

Code Format

The code format is basically just a tokenized scripting language. You can translate the tokens back into their original instructions for displaying the scripting window.  The tokens are listed below, along with how each instruction changes the tab depth (which is used both visually, as well as to determine execution flow).

$80    "IF ("      depth++
$81    "="
$82    "<"
$83    ">"
$84    ") AND ("
$85    ") OR ("
$87    "EXIT\n"    --depth
$88    "END\n"     --depth
$89    "MOVE ("
$8a    ") TO ("
$8b    "PRINT("
$8c    "SOUND("
$8e    "LET("
$8f    "+"
$90    "-"
$91    "*"
$92    "/"
$93    "="
$94    "!"
$95    "MENU("
$a0    "TEXT$"
$a1    "CLICK$"
$b0    "VISITS#"
$b1    "RANDOM#"
$b2    "LOOP#"
$b3    "VICTORY#"
$b4    "BADCOPY#"
$c0    "STORAGE@"
$c1    "SCENE@"
$c2    "PLAYER@"
$c3    "MONSTER@"
$c4    "RANDOMSCN@"
$c5    "RANDOMCHR@"
$c6    "RANDOMOBJ@"
$d0    "PHYS.STR.BAS#"
$d1    "PHYS.HIT.BAS#"
$d2    "PHYS.ARM.BAS#"
$d3    "PHYS.ACC.BAS#"
$d4    "SPIR.STR.BAS#"
$d5    "SPIR.HIT.BAS#"
$d6    "SPIR.ARM.BAS#"
$d7    "SPIR.ACC.BAS#"
$d8    "PHYS.SPE.BAS#"
$e0    "PHYS.STR.CUR#"
$e1    "PHYS.HIT.CUR#"
$e2    "PHYS.ARM.CUR#"
$e3    "PHYS.ACC.CUR#"
$e4    "SPIR.STR.CUR#"
$e5    "SPIR.HIT.CUR#"
$e6    "SPIR.ARM.CUR#"
$e7    "SPIR.ACC.CUR#"
$e8    "PHYS.SPE.CUR#"
$fd    ")\n"
$fe    ") THEN\n"        depth++
$ff    global

Globals are stored in a single byte that follows the $ff opcode.  The names of the globals are represented as A0-A9, B0-B9, C0-C9, and so on.  Thus, globals can only be from $00 to $ea, since $ea is Z9.

Any character in the script without the high-bit set represents itself.  So strings and such are just stored in plaintext.

NOTE: This also includes any constant numbers.  42 is stored as the string "42".

Graphics Data Format

The graphics data found in various assets, including the background, all follow the same format.  It is comprised of a list of shapes, with their corresponding patterns and pens.  The patterns are indices into the PAT# resource, with the first pattern identified with the index 1.

You will need to use the quickdraw routines to accurately draw these shapes.

There is no END code, so just keep parsing and drawing shapes until you hit the end of the graphics section.

Each shape begins with the same header:

struct Shape {
    uint8  fillPattern;  // 00
    uint8  penSize;      // 01
    uint8  penPattern;   // 02
    uint8  shapeType;    // 03
}
enum ShapeType {
    rectangle   = 4,
    roundedRect = 8,
    oval        = 0xc,
    polygon     = 0x10,
    freeHand    = 0x14,
    bitmap      = 0x18,
}

fillPattern
    This is the pattern to use as a fill for the shape.  $1e is reserved to represent no fill at all.
penSize
    This is the size (in pixels) of the pen to use as a stroke for the shape.  $00 is reserved to represent no stroke.
penPattern
    This is the pattern to use for the stroke, $1e is reserved to represent no stroke.
shapeType
    This indentifies which shape to draw.  The shape-specific data follows the header.

Rectangle ($04)

Draws a rectangle at the provided coordinates.  These may be partially or even entirely offscreen.

struct Rectangle {
    Shape header;  // 00
    Rect  bounds;  // 04
}

Rounded Rectangle ($08)

Draws a rounded rectangle with the provided coordinates and corner radius.  This may be partially or entirely offscreen.

struct RoundedRect {
    Shape  header;  // 00
    Rect   bounds;  // 04
    uint16 radius;  // 0c
}

Oval ($0c)

Draws an oval in the provided bounds.  This may be partially or entirely offscreen.

struct Oval {
    Shape header;  // 00
    Rect  bounds;  // 04
}

Polygon ($10)

Draw a polygon

struct Polygon {
    Shape  header;     // 00
    uint16 smoothness; // 04
    uint16 length;     // 06
    Rect   bounds;     // 08
    int16  start_y;    // 10
    int16  start_x;    // 12
    int8   data[];     // 14
}

The bounds aren't used for drawing, but for highlighting the shape in the world editor.  The length determines the size of all the data following the length field, including the length field.

Smoothness determines how many times to subdivide the polygon before drawing it.

The polygon data itself uses a light compression algorithm.  The starting point for the polygon is given in start_x and start_y.  Loop through the data, and if the value is -128, then the next int16 is a new Y coordinate.  Otherwise, add the value to the current Y coordinate.  The next value is for the X coordinate, and so on until the end of the array.

FreeHand ($14)

This shape was actually created using a different drawing tool in the editor, but the storage and appearance is identical to the Polygon.

Bitmap ($18)

Draws a bitmap in the viewport.

struct Bitmap {
    Shape  header;    // 00
    uint16 length;    // 04
    Rect   bounds;    // 06
    uint8  packed[];  // 0a
}

The width of the bitmap is always a multiple of 8.  To unpack the data, you must calculate how many bytes each expanded row will be.  You can simply divide the width by 8 for this.

Next, you will run the algorithm below for each scanline.  Run until you've expanded the correct number of bytes.

The algorithm is a simple RLE format.  The first byte is the opcode.  If it is $80, do nothing and move onto the next opcode.  Otherwise, if the high bit is set, copy the next byte into the output -opcode + 1 times.  If the high bit is not set, copy opcode + 1 bytes from the input into the output.

This is widely known as the PackBits algorithm,  You'll see it referenced as the TIFF or PICT PackBits algorithm.

And that's everything I know about the World Builder file formats.

No comments: