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!
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.
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.
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.
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.
The name of the output file that will contain the archive and the decompressor routine.
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').
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.
A list of input files to compress.
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.
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.
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.
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.
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.
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.
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.
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!
graphics_bombleds.bin (huffman compressed)
The bombleds are a strip of 14 tiles, 4bpp.
graphics_carrybox.bin
The carrybox is two 8x8 tiles of vram pattern data, followed by two more rows for the linkage. There's no compression because it's 64+8 bytes big anyway!!
graphics_contactlosttiles_2bpp.bin (huffman compressed)
The contact lost tiles are a list of 22 tiles, 2bpp. (It's huffman encoded, and then crushed to 2bpp, so it needs to be expanded from RAM to VRAM!)
graphics_count_in_digits_2bpp.bin
2bpp tiles for the large count in numbers when the level begins ingame.
graphics_ending_crate_twoframes.bin (huffman compressed)
Crate ending sprites, 4bpp.
graphics_ending_greenwingsprites_4bpp.bin (huffman compressed)
A large number of 4bpp tiles used to display the 'big ship', the large Greenwing sprite (and the smaller rotated version) shown during the intro, the menu and the end sequence.
graphics_ending_greenwingsprites_map.bin
List of indices into the huffman archive 1 greenwing huffman compressed tiles to allow me to draw the sprites! If you look at it in a hex editor set to eight columns wide, you'll see it describes a grid of numbers showing what sprites go where to show the big ship (and 128 bytes later, the smaller ship). $00 in a byte indicates that there should be no sprite placed at that location.
graphics_ending_spacetiles_2bpp.bin
Space ending background tiles, 2bpp. Also contains the purple atmosphere tiles used during the ending.
graphics_gaiden_ingame_tiles.bin (huffman compressed)
204 8x8 4bpp tiles used for the world scenery display ingame.
graphics_greenfont_2bpp.bin
The greenfont tiles are a list of 80 tiles, 2bpp.
graphics_greenwing.bin (huffman compressed)
Contains all the frames for the greenwing. 3bpp. Must be extracted into RAM first: The top left and top right tiles are complete. The bottom left and bottom right tiles are missing their bottom three rows and for every four-byte bitplaned row of eight pixels, I've omitted the final high byte (all of the colours are in the range 0-7). Have fun reinflating that!
graphics_master_gaiden_letters_2bpp.bin
2bpp MASTERGIDN letters tile reduced.
graphics_plot_1_radioactive_2bpp.bin (huffman compressed)
Radioactive logo 2bpp. Tile reduced. Only one side of the trefoil is given (the other side uses horizontal flip.) (It's huffman encoded, and then crushed to 2bpp, so it needs to be expanded from RAM to VRAM!)
graphics_plot_1_radioactive_2bpp_map.bin
List of indices into the radioactive logo.
graphics_title_gravitybeamlogotiles.bin
Gravity beam logo tiles. 2bpp.
graphics_title_gravitybeamlogotiles_map.bin
List of indices into the above tiles (23 x 8)
levellayout_megatiles.bin (huffman compressed)
This is an 64 across by 48 down array of Megatiles describing the layout of the GBMG level. Zero-based, so remember to use the json to ascii tool.
megatiles_definitions.bin
This is an array of [4byte] structures indicating what 8x8 tiles should be placed into the nametable for the given Megatile. Top left, top right, bottom left, bottom right.
musicmodule_explosionbeep.bin
An SMS music module used to create the whine-hiss sound effect that's played when the crate explodes.
musicmodule_fanfare.bin
An SMS music module played during the end sequence.
musicmodule_ingame.bin
An SMS music module played during normal gameplay. 'The Last Five Minutes.'
musicmodule_plot_1.bin
An SMS music module played on the ALERT screen.
musicmodule_plot_2.bin
An SMS music module played on the 'A desperate call for help' screen, and also the title screen. 'Gravity Beam: Master Gaiden theme: A Desperate Call For Help Is Sent, Greenwing Saves The Day'
sine_table.bin
An array of 128 sine values, a full cycle. 16-bit signed, with range [$003B, $FFC5]. (59, -59 dec). Used for thrust calculation (these are direct contributions to player velocity.)
audio.z80asm
Contains my SMS music player and sound effects engine as a re-usable component, with its own code and ramsections.
segamastersystem.z80asm
Contains all the system defines needed to compile the game, and my controller handling code as a re-usable component. This is not separate from the defines as the system PAUSE button NMI handler directly changes a part of the controller state.
maths.z80asm
Contains complex maths routines such as multiplication, division and square root. All taken from various sources, none written by me.
game.z80asm
Contains everything to do with Gravity Beam: Master Gaiden as a single file.
megatiles_definitions.z80asm
Automatically generated list of megatile-to-graphic tile assignments, produced by 2013megatilemanager.exe.
huffman_archives\huffman_archive_1.z80asm
Automatically generated Huffman archive containing all the Huffman compressed binary data files, together with the decoder.