Gravity Beam: Master Gaiden Documentation

12th April 2013

Compiling The Game

Type make.bat. It's not rocket science.

make.bat is set to compile game.z80asm as object.o, and then link this as output.sms. All other source files are .included by game.z80asm. A symbol table compatible with MEKA is saved as output.sms.sym. output.sms should be a 32kByte Sega Master System compatible ROM image file, with a correct checksum and footer, suitable for directly loading onto a 32kByte EEPROM.

The game uses data from a large number of binary files. If any of these are changed, don't forget to remake the game.

The game expects to find a Huffman encoded archive in huffman_archive_1.z80asm. This file is produced by 2013huffmancatalogencoder.exe as a Huffman compressed form of all the .bin files specified in catalog_file_gfx_greenwing_ending_gaideningame.txt. To regenerate this archive, run huffmanencode.bat. This batch file invokes 2013huffmancatalogencoder.exe on catalog_file_gfx_greenwing_ending_gaideningame.txt, compressing all the files and recreating the archive file. Don't forget to do this if you change any of the files in the archive!


Shims

I've written several shims in C and Javascript to automate things like graphics and level processing.

I use gcc to compile the C programs as Windows commandline executables.

2013huffmancatalogencoder.exe

Reads a plaintext catalog file containing a list of files to compress and outputs a .z80asm file containing all the files huffman compressed according to their nibble distribution, together with a decompressor routine that can output to consecutive RAM locations or out (PORT_VDPData),a, that implicitly holds the huffman decompression table.

Usage

Format: 2013huffmancatalogencoder.exe catalog_file.txt

catalog_file.txt is a plaintext file describing the parameters to construct the compressed archive.

The output file is a Z80 assembly source file containing a data section named prefix_section.

At the top of the file is a large comment block showing a list of all the files included in the archive, their logical data labels, and their sizes in bytes before and after compression. This is followed by a table showing the distribution of input entities (bytes, nibbles) over all input files, their new bit sequence representations, and their estimated complete contribution to the output size. The final output size is listed, together with a guide to how much of my 32kbyte EEPROM was saved as a result of compression.

Next is a series of equates giving the full size in bytes of each file contained within the archive. Then an enumeration giving each file in the archive an index from zero. This is followed by prefix_index, an array of {pointer to compressed data, full data size} pairs of .dw data definitions. The compressed data pointers are named prefix_filename_data. If doubled, the enumeration values can be used as an offset into the prefix_index array.

prefix_decompress is a CALLable subroutine that decompressed BC bytes of data from the compressed data located at HL. DE is the destination address in RAM. If this is set to $0000, the destination is out (PORT_VDPData),a.

Finally come the compressed files themselves as .db desclarations. Each individual file in the archive is byte aligned and accessible through a unique prefix_filename_data label.

Catalog file format

The file consists of a number of directives in all capitals followed by a number of arguments, one on each line. When a new directive is encountered, the following lines are interpreted as arguments to that directive. All filenames are relative to the current working directory.

Lines beginning with a semicolon are comments.

OUTPUT_FILE_FILENAME

The name of the output file that will contain the archive and the decompressor routine.

ASM_TOKEN_PREFIX

A prefix to all tokens produced by the program. It should probably end in an underscore. Convention is to use ha1_ style names (ha stands for 'huffman archive').

COMPRESSION_TYPE

Use byte or nibble to specify whether the data should be compressed as a stream of bytes or nibbles. Defaults to nibble, which is just as well because I haven't implemented 'byte yet.

FILE_LISTING

A list of input files to compress.

Method

The huffman compressor loads into memory all the files specified in the catalogue. It then steps through the files, entity by entity, populating a table of the total population of each element.

To perform the encoding, the program creates a master 'bag' holding tokens representing every possible input entity, and assigns weights to each of these tokens equal to the total population of that entity.

The function huffman_bag_split attempts to split the contents of a bag recursively into two bags (the 'zero-bag' and the 'one-bag') containing tokens of equal weight. When it's called upon the master bag, it splits the master bag into a tree of bags. The decision to place a token into a bag's zero-bag or one-bag sub bags represents the decision to use a 0 or 1 in the bit sequence that encodes this token. The leaf nodes of the tree contain one token each. The location of a token within this tree of bags is dependent on its population: the depth of a token within the tree is proportional to the inverse logarithm of its frequency of the entity within the population.

The function huffman_bag_return_encoding_string traverses the tree and constructs a printable string that encodes the token.

After the encoded files are generated, they are written to the output file together with some boilerplate decompressor code: the code that is constant between invocations of compression is the code that cycles the next bit from the read byte from the input stream into the carry flag, and the code that places completely decoded entities into the output location.

The method by which the compressor selects which tokens go into each bag controls the quality of the compression: the more evenly the tokens are distributed between the bags, the smaller the output will be. At present, the bag split function simply places tokens into one bag until it has a weight greater than or equal to half the total weight of the tokens it has available. The function will also ensure that both sub-bags contain at least one element: this case occurs when we have weights 1-1-1-1-10: the 10 would be added to a bag containing 4 to bring the total weight up to 14, if the function did not abort and place the final element 10 into the second bag explicitly. If the bag split function is presented with a bag containing two elements, it will place one element in each bag.

The decompressor consists of a number of distinct parts: the outer part, which continually requests complete output bytes until the BC register pair is zero; the data reader, which reads bits from the input location one at a time and cycles them into the carry flag; the incoming bit decision tree; and the data output routine. The root of the incoming bit decision tree is called like a subroutine (call ha1_decompress_perform_decompression) by the outer part, asking it to come up with an output byte. The huffman decoding table is implicitly encoded in the incoming bit decision tree: a mass of decisions through which execution flows as it reads each bit from the input bitstream. It contains calls to ha1_decompress_get_next_bit to retrieve the next bit from the input bitstream. When a complete output entity has been recovered, it alters the value in the accumulator and returns. The nibble decompressor needs to recover two nibbles in order to produce a complete output byte.


2013megatilemanager.exe

Given two binary input files, one containing a strip of unique 8x8 tiles (8-bit paletted graphics) that will be placed in the Master System's VRAM and one containing a strip of 16x16 tiles (8-bit paletted graphics) containing logical megatiles that are placed in the Tiled map editor, match up 8x8 subsections of the 16x16 logical tiles against the 8x8 graphic tiles and produce an output binary file containing a list of megatile-corner-to-graphic-tile indices.

This is used to allow me to draw the megatiles with redundant 8x8 corners and place these megatiles in Tiled and then automatically detect which 8x8 graphic tiles are shared between multiple megatiles. The strip of unique tiles should be produced by Usenti's 'reduce tiles' function.

Usage

Format: 2013megatilemanager.exe 8x8tiles.raw 16by16tiles.raw output.z80asm

Open output.z80asm in a text editor and you'll see a list of .db declarations matching megatile corners to 8x8 graphic tiles.


2013smstileformat.exe

Converts image files to an interleaved bitplane format easily displayable on the SMS. This program outputs 4 bits per pixel interleaved binary graphics. It espects the input in the format of a 8 pixel wide vertical strip of tiles.

Usage

Format: 2013smstileformat.exe input.raw output.bin

input.raw is a RAW image file. A RAW image file is a 256 colour paletted image that contains a series of unsigned bytes representing palette indices.

output.bin will be a binary file containing the bitplaned image. The program will overwrite this file silently if it exists.


Megatiles

A Gravity Beam: Master Gaiden level is a 64x48 grid of megatiles. Megatiles are 16x16 pixel sized tiles constructed from a tetrad of 8x8 SMS graphics tiles. The level layout is drawn in the Tiled map editor and stored as a grid of megatile indices. The game collision is done in terms of megatiles: all megatiles below a certain index are passable, all other megatiles are impassable.

The use of megatiles allows the level to be stored at a quarter of the size it would be as 8x8 tiles, at a cost of flexibility.

The use of megatiles requires a system which specifies which base 8x8 tiles a given megatile is constructed from. This is automatically handled by 2013megatilemanager.exe.


Tile drawing routine

As the ingame camera moves around, new tiles are drawn on the incoming edge of the screen by the routines ingame_tiles_draw_column and ingame_tiles_draw_row. Hold onto your butts, because these routines ain't easy.

To draw a row or column on the screen, we need to know which row or column we want to draw (counting in absolute global 8x8 tiles from 0), and what the other coordinate is. These two are used to calculate the starting destination VDP address in the name table for tile drawing, as well as determine the megatile data source start.

Because the routines draw a whole row or column straight across the name table, the routines also need to know where the 'break in the screen' is located. At some point, as the tiles are being drawn into the nametable, we are going to have to rewind all the megatile data sources by the size of an entire screen to account for the way the 'visible tile screen' slides across the static-positioned name table.

Also note that as we draw across the entire length of the name table, drawing always starts at a multiple of 32 tiles and the 'break in the screen' is used to locate the left-most tile relative to the screen.

Both ingame_tiles_draw_column and ingame_tiles_draw_row work in a similar way. The following explanation assumes we are drawing a row.

First, the address of the megatile at the left-most edge of the screen is calculated. This is not forgetting that drawing begins on a multiple of 32.

Then, we acquire the address of the megatiles_corners array. Because we are drawing a row of tiles, we are either constantly reading from the top-most tile definitions or the bottom-most tile definitions. We branch on the row coordinate and select megatiles_corners or megatiles_corners+2 depending on what we're doing. This saves us from having to constantly add 2 to the megatiles_corners as we travel across the row.

Then, the VDP destination name table address is calculated. For drawing a row, we can submit this as a VDP control command as all the addresses we are going to write to are sequential. For column drawing, we need to maintain this destination address value somehow.

Then comes the actual tile drawing loop:

If we have reached the mid-name table split, we rewind the megatile data source pointer by the size of the screen.

Then we retrieve the index of the current megatile under the cursor. If we're currently on an odd numbered tile, the megatile cursor is advanced. This flag is re-used later on to take advantage of the fact that we are alternating between drawing the top-left and top-right tiles as we scan the row.

This is then used to construct the final offset into the megatiles_corners array. We multiply the megatile index by 4 to select the row associated with the current megatile, and then add 1 if we're on an odd X value (and 2 if we're on an odd Y value, but this is implicit due to the above handling of megatiles_corners).

With the address megatiles_corners[...] constructed, we can retrieve the correct graphical tile index associated with this corner of the megatile and write this to the VDP.

Repeat for the width of the screen.

For column drawing, the VDP destination address needs to be reset every tile as the addreses are not contiguous.


Source blob list

Where I've written '2bpp', it means that I've arranged the palette of the graphics data so that all the colours lie in the range 0-3. Use bitwise math to put the bits back into the appropriate bitplanes!


Source file list