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#"
$b3 "VICTORY#"
$b4 "BADCOPY#"
$c0 "STORAGE@"
$c1 "SCENE@"
$c2 "PLAYER@"
$c3 "MONSTER@"
$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
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:
Post a Comment