King's Bounty

Analysis of "King's Bounty - The Conqueror's Quest" for the Genesis/MegaDrive

King's Bounty was a videogame by New World Computing. It was originally released for Apple II and MS-DOS in 1990. Later versions included ports to Commodore 64 and Macintosh, then Amiga. The Genesis/MegaDrive port was released in 1991 with the subtitle The Conqueror's Quest. The port brought some changes: it featured real-time player movement, some different graphics, and even a few changes to game rules.

King's Bounty was a predecessor to the wildly successful "Heroes of Might & Magic" series of games.

About this document

This document describes how the graphics and sounds were stored in the ROM of King's Bounty - The Conqueror's Quest. It is the result of exploratory analysis for my own education and entertainment.

The details in this document could be used for producing a third-party patch to replace graphics or sounds, or perhaps it could be used to add King's Bounty support to a resource viewing application.

The offsets in this writeup are based on the ROM for the USA/Europe release.

  • Length in bytes: 524288
  • CRC32 checksum: aa68a92e
  • MD5 sum: 1bb8e1e5d41bbeff2e452935bddd9883
  • SHA1 sum: 32f90806f44a0bd1d65d84ceeb644681b9cee967
  • SHA256 sum: 5fd1494a6a47e33d85b22ebbc26a1156f31bb539cf54d4e109e1bcbddef8b4eb

Hexadecimal numbers such as ROM offsets will usually be written in the standard 0x123abc format. The abbreviation int16be means a 16-bit (two-byte) integer stored in big-endian order (most significant byte first). Likewise, int32le means a 32-bit (four-byte) integer stored in little-endian order (least significant byte first).

ROM layout

offset contents
0x00000 ROM header
0x00200 Entry code
0x00312 "Jump table" for data and code addresses (272 entries, 570 bytes)
0x00882 Jump table padding (room for an additional 23 entries)
0x0090c Code 1 (math, intro support)
0x00b6e Game data 1 (font)
0x010a4 Code 2 (C code - most of the game logic)
0x18b0c Code 3 (decompression, copying, text printing)
0x19062 Game data 2 (map, text)
0x25174 Code 4 (publisher intro, graphics)
0x285fe Sound driver (Z80 code)
0x29b86 Sound driver support
0x2a060 Code 5 (VBLANK support)
0x2a4b4 Bigger data section (graphics, music)
0x7fa2c Code for checksum

Code 2: Game logic

These functions are the higher-level game code. They follow C calling conventions (args on stack in reverse order, return value in register d0) and even have the name of the function at the end (see elsewhere in this document for the exact format of these names).

A selection of the functions and their C-style calling signature:

offset function name signature
0x047c8 Viewarmy void Viewarmy()
0x05d48 Viewchar void Viewchar()
0x09f9a Music void Music(void *music_sequencing_data)
0x1249e Damage int32_t Damage(uint8_t which_army_slot, uint8_t other_army_slot)
0x164d6 Subtractgold bool Subtractgold(int32_t how_much_gold)
0x167d8 PrintAmount int32_t PrintAmount(int8_t x, int8_t y, int16_t how_many_troops)

Code 3: lower level memory/video routines for game

offset signature
0x18cb6 draw text on at video position int16_t DrawTextAt(x,y, *buf)
0x18d26 text graphic tilerefs indexed by codepoint
0x18b0c decompress (LZSS)
0x18c46 copy main ram to video RAM
0x18ec2 clear rectangle of video ram
0x18f2a copy main ram to main ram (memcpy)

Game data 2: map, text

A selection of some of the contents:

| offset | contents | | | ------- | -------- | | | 0x1a88e | Initial world map (LAND.ORG) | 4 continents * 64 rows * 64 columns | | 0x1ebd3 | Continent names | | | 0x1ecbd | Castle names | | | 0x20d4d | Contract/villain names | | | 0x20ee0 | Signpost text | | | 0x22678 | Town names | | | 0x228d7 | Spell names | | | 0x23766 | Troop quantity bucket labels (A few/Some/Many/...)

For more about the text tables, see the text section of this document.

Code 4: publisher intro

offset
0x280cc decompress function (LZSS body)
0x28190 compressed data (LZSS int16le headers)

Main game data

offset description type
0x2a4b4 Chance of income chest (per continent)
0x2a4d0 Random income from chest (per continent)
0x2a4d4 Fixed income from chest (per continent)
0x2a4d8 Additional spell slots granted from chest (per continent)
0x2a4ea Text alignment for spell name (per spell)
0x2a4f8 Town locations (X coordinate, per town)
0x2a512 Town locations (Y coordinate, per town)
0x2a52c Spell prices 14 * int16be
0x2a5c8 Text alignment for villain names (per villain) 17 * uint8
0x2a5d9 Text alignment for castle names (per castle) 27 * uint8
0x2a7c8 Troop quantity bucket (A few/many/lots/...) cutoffs
0x2a7d2 Troop quantity bucket label string length
0x2a80a Troop max hitpoints indexed by troop type 24 * uint8
0x2a823 Troop skill level indexed by troop type 24 * uint8
0x2a83c Troop movement indexed by troop type (0 = fly) 24 * uint8
0x2a855 Troop min damage indexed by troop type 24 * uint8
0x2a86e Troop max damage indexed by troop type 24 * uint8
0x2a888 Troop gold cost indexed by troop type 24 * uint16be
0x2a8ba Troop morale group indexed by troop type 24 * uint8
0x2a8d3 Troop morale group interactions indexed by morale group 5 * 5 * int8
0x2aba9 Mapping from villain number to villain graphic order
0x2af98 Compressed graphics (credits, intro)
0x33886 Screen border layouts (raw tilerefs)
0x36d86 Compressed graphics (hero portraits, backgrounds, event pictures)
0x4b04c Raw graphics (raw tiledefs) villains
0x57c4c Raw graphics (raw tiledefs) sidebar graphics (siege, magic, minimap, money)
0x5d04c Raw graphics (raw tiledefs) artifacts
0x62cec Raw graphics (raw tiledefs) player character (walk/boat/fly)
0x668ec Raw graphics (raw tiledefs) troops
0x794f0 Music (sequences)

Compression

Much of the graphical data in King's Bounty - The Conqueror's Quest is stored in a compressed form.

The King's Bounty ROM makes use of two different compression formats. Both could be called LZSS (Lempel-Ziv-Storer-Szymanski). They are very similar to each other, but not quite identical.

The main scheme is used for almost everything. It has an 8-byte header. The first 4 bytes is a (big-endian) 32-bit integer counting the length (in bytes) of the expanded data. The other 4 bytes forms another 32-bit integer which counts the length (in bytes) of the compressed data.

There is another scheme, used for some of the resources in the pre-game bootup intro. It has a pair of little-endian 16-bit integers for a header instead.

LZSS compression component Main compression Alternative compression
Uncompressed size header int32be int16le
Compressed size header int32be int16le
Window size 0x1000 (4096) 0x1000 (4096)
Initial window content 0x20 0x20
Initial window pointer 0x0fee (4078) 0x0fee (4078)
Minimum lookback count 3 3

Decompressing the data

The first byte after the header serves as the "flags" for the next eight decompression steps. The eight bits of that byte, starting from the least significant bit, choose between a single-byte literal or a two-byte reference to the window contents. If the flags byte is all-ones, then the eight bytes following are all literals that appear in the decompressed stream (and should be written to the sliding window at the current window position).

The two-byte reference to the existing window contents is made of two parts, a 12-bit window position and a 4-bit window length (beyond the minimum lookback count).

Making your own "compressed" data

You can create data compatible with this scheme that only pretends to compress (and will in fact be bigger than the source data) by inserting a byte with all bits set (value 0xff) at the start, eight bytes of source data, then another 0xff byte, then another eight bytes of source data, and so on. Count the size of your source data stream and your new "compresed" data stream and insert those as headers.

Graphics

A word about graphics on the Mega Drive / Genesis

The video display processor (VDP) does much of its work not at the pixel level but instead with 8x8 groups of pixels. These 8x8 pixel blocks are called "patterns", "tiles", "chars", and probably other names too. In this document, I call them 4bpp VDP graphics tiles.

The tiles are defined and stored in 32-byte definitions, or tiledefs for short. The format is packed 4 bits per pixel (2 pixels per byte), 4 bytes per 8-pixel row.

As an example, a tiledef beginning with the hex bytes 01, 23, 45, 67 could be displayed with color 0 (transparent) in the top-left pixel, color 1 in the next pixel across, color 2 in the third pixel from the left, and so on, finish with color 7 in the top-right pixel.

Each tile is displayed with a 4-bit (16-color) palette line. The are four palette lines, each with 16 color slots (although slot 0 in each line is usually treated as transparent and not shown). Each time a tile is displayed, it must use just one palette line for all 64 pixels.

Tiles are arranged on screen with a series of 16-bit tile references, or tilerefs for short. Other documents call these tilerefs "nametables" or "patterns". Each tileref selects a single 8x8 pixeldef tile for display, along with a palette line number, and whether to flip the pixels horizontally or vertically. Some games can use this flipping and recoloring to do all kinds of tricks, but King's Bounty tends to be pretty straightforward with its display.

Troop graphics

King's Bounty has 24 types of military unit available in the game (Peasants, Militia, Archers, and so on). Each troop type is represented by a 4-frame animation. The graphics are 6x4 graphics tiles (48x32 pixels).

The graphics are stored at ROM offset 0x668ec as raw (uncompressed, headerless) VDP graphics tile definitions. Each frame has 6 * 4 = 24 tiles, and since each tile is 32 bytes, a frame is 24 * 32 = 768 (0x300) bytes. Four frames of 0x300 bytes means the animation for each troop type is 0xc00 bytes, and the animations for all 24 troop types is 0x12000 bytes.

The tiles are stored in column-major order (like villains). That is, the tiles stored as T1 T2 T3 T4 T5 T6 T7 T8 T9 ... T24 are arranged as in the figure below:

left right
T1 T5 T9 .. .. ..
T2 T6 .. .. ... ..
T3 T7 .. .. ... T23
T4 T8 .. .. ... T24

All troop graphics use the standard palette (the one stored compressed at ROM offset 0x3371a). The array of 16-bit words at ROM offset 0x2adc6 (selected by jump table -0x1e52) determines which palette line to use for each troop graphic. The values are in the same format used for tilerefs. A value of 0x0000 means palette line 0, 0x2000 means palette line 1, 0x4000 means palette line 2.

The animations are stored in order of the troop type number, the same as the text table beginning at 0x237c0 (jump table + 0x117).

Villain graphics

Similar to the animations for troops, King's Bounty has 17 villain animations.

Like troop graphics, villain graphics are stored as raw (uncompressed, headerless) 4bpp VDP graphics tile definitions. They begin at ROM offset 0x4b04c and end at 0x57c4c.

There are animated graphics for 17 different villains.

The tiles are stored in column-major order (like troops). That is, the tiles stored as T1 T2 T3 T4 T5 T6 T7 T8 T9 ... T24 are arranged as in the figure below:

left right
T1 T5 T9 .. .. ..
T2 T6 .. .. ... ..
T3 T7 .. .. ... T23
T4 T8 .. .. ... T24

Like troop graphics, each villain has a loop of 4 animation frames.

  • Each villain uses a single palette line but different villains use different palette lines
    • There is an array of 16-bit words at ROM offset 0x2adf8 that select which palette line
    • This array is in the same order as the villain graphics, not ordered by "villain number" (see note below)
    • The palette values are in the format used in tilerefs: 0x2000 means palette line 1, 0x4000 means palette line 2
  • Each frame is 0xc00 bytes (4 frames * 6 columns * 4 tiles * 32 bytes) making each animation 0x3000 bytes.
  • Each frame is stored as 6 columns of 4 tiles (column-major)
    • Each column of tiles is stored top-to-bottom
    • Each tile represents 8x8 pixels
    • Each tile is a 32-byte pattern

The villain graphics not ordered by "villain number", so don't expect them in the same order as the table at ROM offset 0x20d6a. However, there is a mapping from villain number to graphics number in the 17 bytes at ROM offset 0x2aba9. The first byte at ROM offset 0x2aba9 is the graphic number (16) for the first villain (Murray). You can expect the animation data for Murray to begin at ROM offset 0x4b04c + (0x3000 * 16).

Event pictures

In addition to the smaller animated graphics, King's Bounty contains some bigger still images, like the intro screen and the ones shown visiting somewhere for recruiting troops.

These bigger images are stored compressed in three parts: the tile definitions, the palette definitions, and the tile references that show which tiles are displayed with which palette.

tiledefs tilerefs palettes used for
0x2ef20 0x308dc 0x30e24 opening title logo
0x2b28a 0x2e774 0x2eeae character select
0x44f3c 0x469d8 0x3371a castle
0x3a3e0 0x3bec6 0x3371a town
0x3f346 0x40db2 0x3371a plains
0x3c2c2 0x3ef42 0x3371a forest
0x41064 0x43296 0x3371a hill
0x43696 0x44b70 0x3371a dungeon
0x496be 0x4ac7a 0x4afcc endgame lose
0x47f5a 0x4932e 0x49648 endgame win

All compression is the same LZSS format with an 8 byte header made up of a pair of 32-bit long integers (big-endian). The first is the count of compressed bytes, while the other is the count of uncompressed bytes. The compression algorith is described in more detail elsewhere in this document.

After uncompressing, the tiledefs have a 2 byte header, a single 16-bit short integer (big-endian). That header is the count of tile definitions, and should equal the remaining bytes in the buffer divided by 32 (bytes per tiledef).

Likewise, the uncompressed tilerefs have a 4 byte header: a pair of 16-bit short integers (big-endian). That header is the count of rows and columns of tilerefs. The first number is rows (image height), the other is columns (image width).

The palettes, after uncompressing, are in the raw CRAM format. The buffer is 128 bytes: 4 palette lines, each 16 color entries, each entry a 16-bit short integer (big-endian). The entries represent RGB values, or rather BGR: 0x0eee is pure white, 0x00e is max red, 0x0e0 is max green, 0xe00 is max blue.

Terrain graphics

King's Bounty shows two different scales of terrain: in battle and out of battle. Terrains like like grass, trees, mountains, and lakes are shown in both. Despite some quite similar graphics, they are powered by entirely different definition data.

Battle terrain

The terrain graphics shown in each battle tile is made up from 6x5 graphics tiles (48x30 pixels).

The tile definitions are stored compressed at ROM offset 0x46d90, with a sixteen-bit count header on the decompressed stream.

The tile references are stored raw and headerless starting at ROM offset 0x19666.

Overland terrain

The initial world map is defined at ROM offset 0x1aa8e and covers four continents. Each continent is 64x64 map tiles (not graphics tiles). Each map tile is a byte and each byte value has a meaning. For example: a blank square of grass is 0x00, a blank square of water is 0x20, a square of water with a shoreline on the left is 0x1c.

The graphics tiles for these map tiles are stored compressed (with a 16-bit header for graphics tile count) at ROM offset 0x30e82. Each map tile is displayed as a 6x5 arrangement of graphics tiles. The tile references are in 60-byte groups (6x5 16-bit short integers), starting at ROM offset 0x1980a. There are 80 different map tiles.

The palettes are the usual set (found compressed at ROM offset 0x3371a).

Castle siege battle terrain

When the player lays siege to a castle, a battle begins. The background for such a battle does not use battle terrain as described above, but rather a single large image. The image is stored in a similar way to hero portraits or event pictures. The tile definitions are stored compressed at ROM offset 0x46d90, with a two-byte header once decompressed. The tile references are stored compressed at ROM offset 0x47cc6, with a four-byte header once decompressed (height, width).

Hero portraits

Each of the four different character class has a different "hero portrait" picture to show on the View Character screen. Each portrait is 12 graphics tiles wide by 13 graphics tiles high (96x104 pixels).

The hero portraits are stored in a similar way to event graphics: a set of 32-byte tile definitions (stored compressed with a header), combined with a set of 2-byte tile references. There are some differences: the tile references are stored uncompressed headerless, row-by-row (row-major), and each row is padded with 0xffff in unused references.

The compression is the same LZSS compression used by event graphics and other parts of the game. The palettes are the usual set (found compressed at ROM offset 0x3371a).

Portrait addresses in ROM:

tiledefs tilerefs palettes class
0x36d86 0x37a0e 0x3371a Knight
0x37b7a 0x3872e 0x3371a Paladin
0x3889a 0x394b6 0x3371a Sorceress
0x39622 0x3a274 0x3371a Barbarian

User interface graphics

Puzzle map

The full-screen puzzle map is displayed in a frame with tilerefs stored in the compressed data at ROM offset 0x3379e.

Each cell in the puzzle grid is filled with one of three graphics:

  • villain graphic/animation (the set beginning at ROM offset 0x4b04c )
  • an artifact graphic (the set beginning at ROM offset 0x5d04c), or
  • an overland map graphic (tilerefs beginning at ROM offset 0x1980a, tildefs stored compressed at ROM offset 0x30e82)

Player sprites

  • Raw (uncompressed, headerless) graphics tiles stored in column-major order
  • All are displayed with palette line 0 of the standard palette (the one stored compressed at ROM offset 0x3371a)
  • One set for walking on land, one set for sailing on boat, one set for flying
  • Each frame is 6 * 4 = 24 tiles (24 * 32 = 0x300 bytes per frame)
  • Each set has 8 frames (8 * 0x300 bytes per frame = 0x1800 bytes per animation set)
  • The walking set begins at ROM offset 0x62cec
  • The boat set begins at ROM offset 0x644ec
  • The flying set begins at ROM offset 0x65cec

Artifact graphics

  • Eight artifacts with a hard-coded order and meaning
  • Raw (uncompressed, headerless) graphics tiles stored in column-major order
  • One set wider (6x4 tiles), one set narrower (5x4 tiles but still the same number of bytes)
  • The wider set starts at ROM offset 0x5d04c, and is used for the Puzzle Map view
  • The narrower set starts at 0x5e84c, the same place that the "ninth artifact" would, and is used in View Character for artifacts the player possesses
  • The graphics are ordered by artifact number, the same order as the text strings which give each artifacts their name are found with (jump table + 0x41c) which winds up being at ROM offset 0x22108
  • Each artifact is displayed with a single palette line from the standard palette
    • The palettes lines are stored in an array of 16-bit words at ROM offset 0x2ae1a (found by jump table - 0x1e4a)
    • The first 16-bit word in that array is 0x2000, so the first artifact uses palette line 1
    • The palette values are in the format used in tilerefs: 0x2000 means palette line 1, 0x4000 means palette line 2, 0x0000 means palette line 0

Continent maps

The player starts with a map to the first continent. The View Character screen represents this with a 6x4 graphic with tiledefs at ROM offset 0x6124c (raw, headerless, in column-major order). Each of the following 0x300 bytes serves as tiledefs for a graphic for another three continents, followed by a blank continent map. The blank continent map is used on the View Character screen for inaccessible continents.

All maps are displayed with palette line 1.

There is an exact duplicate set at 0x6034c, but that one does not seem to be used anywhere.

Side pane

The side pane shows some game status with five equally-sized graphical items. Each item is 6x4 graphics tiles in size (48x32 pixels). Some show animations, while the others are still images. All are stored as raw (not compressed) tile definitions, and most use palette line 1 from the standard palette (the one found compressed at ROM offset 0x3371a).

If a player holds a contract for a particular villain, that villain's animation is displayed as described in the Villain graphics section. If the player does not hold a contract for any villain, the still "no contract" graphic defined at ROM offset 0x5944c will be used instead. The tile definitions are stored column-major order, and they are displayed with a single palette line (1?) from the standard palette.

If the player has siege weapons with them, then an animation defined at ROM offset 0x57c4c will play instead of the "no siege weapons" graphic defined at ROM offset 0x5974c. This animation is 4 frames long, stored like a villain or troop animation. It is displayed with palette line 1 of the standard palette.

If the player has the ability to cast magic spells, then an animation defined at ROM offset 0x5884c will play instead of the "no magic" graphic defined at ROM offset 0x59a4c. The animation is four frames long, stored like a villain or troop animation. The palette lines used for display include 1 but it's mostly just flashing different colors so other palette lines also work.

(TODO: Check what palette lines the game actually uses for the active magic animation.)

The mini-puzzle is a short representation of the state of the puzzle map (that is, it changes colour each time the player claims a bounty on a villain or when the player finds an artifact). It is made of 6x4 graphics tiles like the other sidebar entries. The raw tiledefs are arranged in column-major order at ROM offset 0x59d4c and displayed with palette line 1. This graphic is drawn with a grid of red rectangles to obscure any pieces not found.

The player's money is shown as stacks of gold/silver/copper coins overlaid on a background image of a money pouch.

  • The money-pouch background graphic tile definitions are raw headerless and begin at ROM offset 0x5a04c
  • Piles of gold, silver, copper coins are built up by overlaying 1x3-tile (8x24 pixel) sprites with transparent backgrounds
  • Overlaid coin tile definitions begin at ROM offset 0x6214c and are stored raw and headerless
  • All the money graphics use palette line 0 of the usual palette set

Summary

Graphic Tiledefs Tiledef header Tilerefs Tileref header Palette Palette line
Troop Raw Headerless Column-sequential N/A Standard Varies per troop
Villain Raw Headerless Column-sequential N/A Standard Varies per villain
Artifact Raw Headerless Column-sequential N/A Standard Varies per artifact
Money Raw Headerless Column-sequential N/A Standard Fixed
Player sprite Raw Headerless Column-sequential N/A Standard Fixed
Continent maps Raw Headerless Column-sequential N/A Standard Fixed
Overland terrain Compressed 1x int16be Raw Headerless Standard In tilerefs
Battle terrain Compressed 1x int16be Raw Headerless Standard In tilerefs
Hero portrait Compressed 1x int16be Compressed Headerless Standard In tilerefs
Castle siege Compressed 1x int16be Compressed 2x int16be Standard? In tilerefs
Event Compressed 1x int16be Compressed 2x int16be Compressed In tilerefs

Music and sound effects

The Mega Drive/Genesis port of King's Bounty uses a sound driver by Electronic Arts' Steve Hayes.

The sound engine is listed on Game Development Research Institute's wiki as being "Electronic Arts/Steve Hayes", the same as several other titles: Populous, Marble Madness, Zany Golf, Buck Rogers: Countdown To Doomsday, and more. Some other sound engines (notably GEMS) have tools to extract the music data. It is possible something like this could exist for the Steve Hayes engine.

http://gdri.smspower.org/wiki/index.php/Mega_Drive/Genesis_Sound_Engine_List

The sound driver itself runs on the Z80 processor, rather than the Motorola 68000 CPU that runs the main game code. The Z80 code is stored at ROM offset 0x285fe-0x29b86 (0x1588 bytes in length). The code for the two processors does some light interaction to coordinate playing and finishing, as well as loading batches of music and sound effects.

Music

Music in King's Bounty is sequenced in a format conceptually similar to MIDI: Assign instrument to track. Select a key by note number, send a "key on" for that track to that note number, delay, key off.

Note numbers are counted in semitones like piano keys. In General MIDI, note number 60 (0x3c) represents middle C, and that seems to be true in Steve Hayes music sequencing format too.

Track 9 is a special track where the note numbers represent different (percussion) sounds. It is similar to General MIDI drum note numbers ("Channel 10"), and in fact the specific numbers seem to map well enough.

offset length purpose
0x2a486 0x0005 (Initial empty music)
0x794f0 0x0fac Walking
0x7a4a4 0x0f0d Battle
0x7b3b8 0x04cd Meeting
0x7b88c 0x3975 Intro/Endgame
0x7f208 0x05b2 (Long silence)
0x7f7c0 0x019b Battle Won(?)
0x7f960 0x00c9 Battle Failed

King's Bounty only uses few of the instruments defined in the Steve Hayes sound driver. There are many more that are defined but are never assigned to any track.

Music sequencing format

The music data is a stream with commands and arguments, interspersed with delay lengths. Most delay lengths are zero (don't wait before processing the next command) but non-zeroes are used for musical timing.

Commands in the high nybble of the first byte:

  • 0xc assigns instrument to a track
  • 0x9 key on (note begin)
  • 0x8 key off (note end)
  • 0xf special
Command 0xc - assign instrument
Cx yy: set track x to use instrument yy
C0 00: set track 0 to use instrument 0
C2 05: set track 2 to use instrument 5
Command 9 - key on (note begin)
9x: key on (note begin) for a track
90 yy zz: key on track 0, note yy, velocity zz
91 yy zz: key on track 1, note yy, velocity zz
92 yy zz: key on track 2, note yy, velocity zz
...
99 yy: play sample for note yy (like drum track)

Drum track notes:

drum track FM instrument PSG wav notes in
"anchorage" 0xba8 0x101c 0x24, 0x29
"bangladesh" 0xc34 0xc42 0x26, 0x2d
"cincinatti" 0xbf0 0xe20 0x2a, 0x2c, 0x30, 0x36
"dublin" 0xbcc (none) 0x2e, 0x31, 0x33, 0x2e

(TODO: Identify actual percussion names like snare, bass drum, etc for placeholder code names)

Command 8 - key off (note end)
8x yy zz: key off (note end) on track 0, note yy, zz is ignored (i think)
Command 0xf - special
Fx: special
FC: end of stream

Musical instruments

There are two kinds of instruments - one played on the FM synth and one played on the PSG (programmable sound generator).

The FM synth instruments are stored in a structure with register values for a YM2612 channel. They are spaced 36 bytes apart but the actual structures are 46 bytes, relying on the unused bytes at the start of the following instrument definition to store the higher values for the current instrument.

byte # YM2612 register meaning
12 30 mul/dt op1
14 38 mul/dt op2
13 34 mul/dt op3
15 3c mul/dt op4
16 40 tl op1
18 48 tl op2
17 44 tl op3
19 4c tl op4
20 50 ar/rs op1
22 58 ar/rs op2
21 54 ar/rs op3
23 5c ar/rs op4
24 60 dr/am op1
26 68 dr/am op2
25 64 dr/am op3
27 6c dr/am op4
28 70 sr op1
30 78 sr op2
29 74 sr op3
31 7c sr op4
32 80 rr/sl op1
34 88 rr/sl op2
33 84 rr/sl op3
35 8c rr/sl op4
36 (unused)
37 (unused)
38 (unused)
39 (unused)
41 a4 freq hi
40 a0 freq lo
44 b0 algorithm
45 b4 panning/pms/ams

The sound driver includes several instrument presets:

instrument # offset (sound) offset (ROM)
0 0x10fe 0x285fe + 0x10fe
1 0x1122 0x285fe + 0x1122
2 0x1146 0x285fe + 0x1146
3 0x116a 0x285fe + 0x116a
... ... ...
31 0x155a 0x285fe + 0x155a

These instrument presets packed together somewhat -- the final bytes of instrument 2 are after the starting offset for instrument 1. This is not a problem because the first 11 bytes of the instrument definition are not read.

PSG envelopes

The PSG ("programmable sound generator") instruments are stored as 2-byte length header followed by a series of 4-bit attenuations (2 attenuations per byte).

Attenuation is like the opposite of volume: zero attenuation is loud, attenuation 1 is a little quieter, max attentuation is silent. The header is a 16-bit short integer (little-endian) for the number of bytes after the first byte of attenuation.

PSG waveform offset (sound) offset (ROM)
mizle bangladesh 0x0c42 0x285fe + 0xc42
mizle cincinatti 0x0e20
mizle anchorage 0x101c

Sound effects

There is a table of 32 pointers at ROM offset 0x29f10.

The first entry points to ROM offset 0x29f90 and the final points to ROM offset 0x2a054.

sound offset used for
0 0x29f90 Raise Control spell
1
2 Time Stop spell
3
4 Found a map in a chest, Instant Army spell
5 Money: Found treasure or income in a chest
6 Magic: Found magic in a chest (spell slot, spell power, spell)
7 Error message
8 Town Gate spell, Castle Gate spell
9 Resurrect spell
10 (unused?)
11 (unused?)
12 Freeze spell
13 Bridge spell
14 (unused?)
15 (unused?)
16 Damage: Turn Undead spell, Fireball spell, Lightning spell
...
31 Artifact

The sound effects themselves are a sequence of 23 bytes (they may overlap). Byte 0 gets copied to the first byte of an "instrument" and byte 1 is used as a note number to play. The following two bytes form a 16-bit Z80 pointer to a byte.

TODO: Identify the specifics of the bytes in a sound effect definition. They might well overlap with FM instrument definition.

Text

The font used for text is stored as LZSS-compressed tile definitions (tiledefs). The comprssed tiledefs can be found at ROM offset 0x6be. These are loaded to VRAM address 0xf000 which is tile number 0x780.

There is an array of 128 16-bit tilerefs starting at ROM offset 0x18d26 which maps ASCII values to tileref values (as in select which tile).

The game credits are stored like an event picture, rather than as ASCII-style strings. The game credits are stored as compressed tilerefs at ROM offset 0x2af98, with a height/width header just like event pictures.

Most of the rest of the text in the game is stored as strings. These strings use a 7-bit character encoding that is mostly ASCII-compatible. Strings are usually terminated either with a null byte (0x00) or with the special byte 0xa5. Many strings are stored in fixed-width structures, padded with spaces and/or null bytes.

Some of these text tables are listed below. For example, continent names are in a text table at ROM offset 0x1ebd3 (found by looking up the address 0x29c bytes after the start of the jump table at 0x312). Each entry is a fixed-length ASCII string 12 bytes long, and there are 5 of them (although there are four continents, this table has a fifth entry for "Unknown").

text for ROM offset jump table size
Continent names (left-aligned) 0x1ebd3 + 0x29c 5 * 12
Continent names (centred) 0x1ec0f + 0x13e 4 * 12
Artifacts (text on finding) 0x22108 + 0x41c 8 * 6 * 28
Town names (by number) 0x22678 + 0x32c 26 * 15
Villain names 0x20d6a + 0x2f0 17 * 22
Castle names 0x1ecbd + 0x308 29 * 16
Troop names (plural) 0x237c0 + 0x116 25 * 11
Troop names (singular) 0x238d3 + 0x246 25 * 11
Signpost text 0x20ee0 + 0x27c 79 * 2 * 29
Respawn (battle loss) text 0x2445e + 0x26c 7 * 29

Tip: When the size lists three numbers, the middle number is the number of lines.

Some text tables have matching numeric tables with the lengths of each string. For example, the castle names are at 0x1ecbd and the byte array at ROM offset 0x2a59d contains the lengths. Likewise, the byte array at ROM offset 0x2a5c8 covers the lengths of the villain names at ROM offset 0x20d6a. If you change the string, you will probably need to change the length to keep the display aligned.

RAM layout

jump table a5 RAM address what type
-0x1b62(a5) 0xffe7b0 spell power int8_t
-0x1b60(a5) 0xffe7b2 max learnable spells int8_t
-0x1b5e(a5) 0xffe7b4 villains captured by villain number bool[17]
-0x1b4c(a5) 0xffe7c6 artifacts found by artifact numer bool[8]
-0x1b28(a5) 0xffe7ea troop type by army slot int8_t[5]
-0x1b18(a5) 0xffe7fa player location continent int8_t
-0x1b12(a5) 0xffe800 player location x int8_t
-0x1b10(a5) 0xffe802 player location y int8_t
-0x1aba(a5) 0xffe858 castle ownership by castle number int8_t[26]
-0x1a6c(a5) 0xffe8a6 location of goal (continent, x, y) int8_t[3]
-0x1266(a5) 0xffed9a castle guard troop type by castle number int8_t[26][5]
-0x4dc(a5) 0xfffb24 castle guard population by castle number int16_t[26][6]
-0x4ec(a5) 0xfffe26 leadership int16_t
-0x4ea(a5) 0xfffe28 weekly income int16_t
-0x4e6(a5) 0xfffe2c troop population by army slot int16_t[5]
-0x3f8(a5) ? ? ?
-0x3d6(a5) 0xffff3c days remaining to find goal int16_t
-0x3c2(a5) 0xffff50 gold on hand int32_t

Save games (passwords)

  • Game state is saved as long password strings rather than files
  • Passwords are displayed as 56 alphanumeric characters across 7 lines of 3-2-3 characters
  • Each character holds 5 bits of information
    • although 36 characters are available for input, the game replaces several to use a smaller set:
      • '1' -> 'I'
      • '0' -> 'O'
      • '5' -> 'S'
      • 'U' is out of bounds
    • then those spots are filled by further replacement from the end
      • '7' -> 'U'
      • '8' -> '1'
      • '9' -> '5'
    • The coding is stored at ROM offset 0x2aae2
      • 'A' is 0
      • 'B' is 1
      • '6' is 31
  • There is some scrambling and a checksum
  • The scrambling has two passes of XOR, one constant key and one with a variable key
    • The outer pass (first when decoding) XORs each byte in a pattern equivalent to the following hex key:
      • 11 12 1c 1d 1f 18 1a 1b 1d 1e 18 19 1b 04 06 07 09 0a 14 15 17 10 12 13 15 16 10 11 13 1c 1e 1f 01 02 0c 0d 0f 08 0a 0b 0d 0e 08 09 0b 14 16 17 19 1a 04 05 07 00 02 03
    • The inner pass (first when encoding) takes the value of the twelfth byte and XORs each of the other bytes with that value
  • A checksum is stored in the twenty-third byte of the unscrambled password as well as two of the bits in the twenty-sixth byte
    • checksum = unscrambled_password[0xb] | ((unscrambled_password[0x16] & 0x18) << 2)
  • The checksum is the (rolling 7-bit) sum of each byte in the unscrambled password and also each of the elements in a special unpacked loading structure
  • The specially-unpacked loading structure is an array of 20 16-bit words (called a4 here) unpacked from the first 24 bytes of the unscrambled password (called a3 here)

    a4[0] = a3[0] & 3; // rank
    a4[2] = (a3[0] & 0xc) >> 2; // character class
    a4[4] = (a3[0] & 0x10) >> 4; // fast search (bool)
    a4[6] = (a3[1] << 10) | (a3[2] << 5) | (a3[3]); //villains captured (bits)
    a4[8] = a3[4] & 3; // difficulty
    a4[0xa] = (a3[4] & 0xc) >> 2; // continents discovered
    a4[0xc] = (a3[5] << 9) | (a3[6] << 4); //weekly income
    a4[0xe] = (a3[7] << 9) | (a3[8] << 4) | ((a3[9] & 0x18) << 0xb); // maybe resurrected ghosts count
    a4[0x10] = (a3[9] & 7) | (a3[0xa] << 3); // time left (saves as weeks, but loads as days?)
    
    a4[0x12] = (a3[0xd] << 9) | (a3[0xe] << 4) | ((a3[0xf] & 0x18) << 0xb); // some leadership thing maybe points required for next rank
    a4[0x14] = (a3[0xf] & 0x4) >> 2; // has learned magic
    a4[0x16] = (a3[0xf] & 0x2) >> 1; // has siege weapons
    a4[0x18] = (a3[0xf] & 1) | (a3[0x10] << 1) // learnable spell count (stored as half)
    a4[0x1a] = (a3[0x11] & 0xf);
    a4[0x1c] = (a3[0x12] & 0xf);
    a4[0x1e] = (a3[0x13] & 0xf);
    a4[0x20] = ((a3[0x11] & 0x10) >> 1) | ((a3[0x12] & 0x10) >> 2) | ((a3[0x13] & 0x10) >> 3) | (a3[0x14] & 1);
    a4[0x22] = (a3[0x14] & 0x1e);
    a4[0x24] = (a3[0xc] << 5) | a3[0x15];
    a4[0x26] = (a3[0x16] & 7) | (a3[0x17] << 3); // artifacts found (bits)

A run of five bytes starting at a3[0x18] is the troop type for each of the player's five army slots. The value 31 ('6') represents no army in that slot.

The next pair of five-byte runs (starting at a3[0x1d] and a3[0x22]) encode the number of troops in each of the player's five army slots.


    troop_count[i] = (a3[0x1d + i] | (a3[0x22 + i] << 5)) << 2

The byte at a3[0x27] is a random number, just there for checksum/scrambling.

The 15-byte stretch starting at a3[0x28] are the carried spells.

Gold on hand is a combination of the pair of bytes at a3[0x36] and a3[0x37].


    gold = (a3_36 << 15) + (a3_37 << 9)

One might expect there would be a third byte for the lowest 8 bits but those bits don't seem to appear anywhere in the password. There is evidence that gold was planned to be stored with dynamic precision but the real released passwords are fixed at 10 bits at a 512-gold resolution.

Code

Much of the game code appears to have written in the C language.

From around 0x10a4 to 0x18b0b, there are a bunch of C functions that follow the same conventions:

  • Each function begins with bytes 0x4e 0x56 (link a6, nnnn)
  • The a6 register is used as base pointer for the function (the stack at call-time)
  • The a7 register is used as the stack
  • The function is responsible for restoring any registers that it writes to beyond the volatile list: a0, a1, d0, d1
  • All arguments are on the stack, never by register
  • The function signals a return value (if any) by writing to the d0 register
  • The a5 register is a constant pointer to the start of the jump table (at ROM offset 0x312)
  • Positive a5 offsets look into the jump table
  • Negative a5 offsets are for accessing static global variables in RAM
  • Each function ends with the bytes 0x4e 0x5e 0x4e 0x75 (unlk a6; rts) and then its label
  • The label is a length byte (with the high bit set) followed by a null-terminated ASCII string (there may be additional null bytes for alignment)

Some other functions seem to follow most of the same conventions but lacks the label at the end.

Multiplication tip

A pattern that occurs frequently in the code that tripped me up in the early days was a pair of multiplications, a pair of swaps, a clear and an addition. An example is the sequence of bytes:

    2001 c2fc0019 4840 c0fc0019 4840 4240 d280

which is the assembled version of the following:


    move.l d1, d0
    mulu.w 0x19, d1
    swap d0
    mulu.w 0x19, d0
    swap d0
    clr.w d0
    add.l d0, d1

I believe this was compiled from a fragment of C code that read something like i * 0x19 in a fragment like the following:


    int i;
    for (i = 0; i < 5; i++) {
      Print(messages[i * 0x19], 2, i);
    }

Not covered

  • RAM layout at runtime
  • The publisher/developer intro animation at game boot
  • Specific game logic (eg damage calculations)
  • Translation/modding guide for text etc
  • Anything about the author
  • Who to contact for corrections or clarifications etc