Phoenix 7.6 by Patrick Davidson - Internal Documentation This is the internal documentation for the game Phoenix. For information on using the game, see the file 'PHOENIX.TXT'. This document explains the internal workings of the program. It is mainly intended for people who want to modify the game. _____________________________________ Table of Contents 1. Introduction ..................................................... 18 2. Building the program ............................................. 50 3. Overview of data structures ...................................... 71 4. Overview of program flow......................................... 226 5. File-by-file description ........................................ 294 6. How to create new levels ........................................ 448 7. How to create new enemies ....................................... 495 8. Coding and commenting style ..................................... 547 _____________________________________ Introduction Phoenix is free/open source software. This means that everyone is allowed to develop modified versions of the game. I chose this for the simple reason of trying to maximize the usefulness of the program. This provides many benefits, such as: 1) Allowing people who want to have a slightly different game to make it themselves with a minimum of effort. 2) Allowing intermediately-skilled programmers to learn from the design of the game. 3) Allowing the more advanced programmers to develop substantially different games based on this one more easily than writing new games from scratch. 4) Allowing the program to continue even if I stop supporting it myself. However, even though several people have talked to me about making such modified games, nobody has actually released one yet. To try to make it easier for people to do that, I am releasing this document which describes the internals of the game, and have also added many additional comments to the code itself. This document only provides an overview of the working of the game. More detailed information about specific functions is present in the comments of the source files themselves. This document assumes that the reader has at least basic familiarity with programming the 68K-based calculators in assembly. If you are a complete beginner, this is probably not the best resource for learning to program the calculators. ______________________________________ Building the program The same source files are used for all three versions of the game. The target is selected by the file 'WHICH.H' which identifies the calculator to compile for. The source files contain conditional code to assemble correctly for all three calculators. All three versions are built by assembling the main source file 'PHOENIX.ASM' which includes all of the other necessary files. For the TI-92, the developer version of Fargo obviously should be installed. For the other calculators, you must have the 'TIOS.H' supplied with TEOS, and I recommend using makeprgm.exe as a linker, as it's more reliable than the DoorsOS one. The supplied batch file 'BUILD.BAT' will build for all three calculators automatically, and creates ZIP archives of each calculators files. This assumes that the Fargo build tools are in your path, and the TEOS environment variable points to where TEOS is. There is now also a shell script 'build.sh' to do the same thing under UNIX. Read the comments at the beginning of it to see what you must install to build with it. ______________________________________ Overview of data structures All of the game's variables, as well as the screen buffer, are kept on the stack. When the game starts, this space is allocated on the stack, and the A5 register points to the start of this space (where the screen buffer is). Additionally, A6 always points to $600000, the beginning of the I/O registers. The data structures on the stack are defined in 'PHOENIX.H'. It begins with definition of the structures for player bullets, enemy bullets, and enemies. These definitions define the offsets of the various data items regarding each in the records for them, and the size of each record. The data for each is stores in arrays on the stack. After the structure structure definitions are the main variables. This is done with a macro that reserves specified amounts of space for each variable. For the most part, the comments describe the variables fairly well; where they're unclear, the exact use of a variable is explained in the code that uses it. Included in this data are arrays for bullets, enemy bullets, and enemies. Each of these array simply consists of one record followed by another. On all calculators, the display buffer is 5120 bytes in size. This represents a display of size 256 by 160 pixels. The coordinates which are actually displayed are: TI-92 and TI-92 Plus (16, 21) - (239, 140) TI-89 (48, 21) - (207, 119) As expected, this is with (0, 0) being the upper left corner. The area the player can move into is: TI-92 and TI-92 Plus (16, 96) - (239, 140) TI-89 (48, 96) - (207, 121) Since the data structures for the player, bullets, enemy bullets, and enemies are all very similar, I will describe them all here. Where there are more than one item of a certain type, the structures are stored in a simple array with one element after the next. Here is a table of where the data is, as offsets from the variable start in A5, like all variables: Offset Structure size Number of items player_ship 16 1 enemies_data 18 26 enemy_bullets 16 16 bullets_data 16 num_b (varies from 16 to 24) The "object" data structure has the following fields (for the player's ship, the names are different, but the offsets are the same): ?_type offset 0 ?_dmg offset 2 ?_x offset 4 ?_w offset 6 ?_y offset 8 ?_h offset 10 ?_image offset 12 ?_data offset 14 These structures are all defined in PHOENIX.H. The ? is 'b' for player bullets, 'e' for enemies, and 'eb' for enemy bullets. Even though you can use the numerical offsets, it is generally a better idea to use the defined symbols for the offsets instead, to increase readability and decrease the chance of errors. Also, ?_size is defined to be the total size of the structure. When moving between structures or referring to the previous/next one, you should always use these values so that the code will still work if the structure size is changed. While it's unlikely that anyone will ever remove or rearrange any existing fields, there's a much better chance of fields being added. The first item, ?_type, specifies what type the object is. If this type is zero, that means that this slot in the array is empty; an object is destroyed by setting this to 0. For the player's ship only, the type is not used in this way but instead is simply a number from 0 to 2 which is the number of the ship the player has. If the type is not zero, it is an offset for the code to control the object. All objects are defined with code to control them; this control code will update the object's coordinates, and can also change things like the type (to switch control code) as well as image and size if appropriate. This system is designed to give maximum flexibility to each type of object; adding a new type of behavior is as simple as writing one new control routine. EXCEPTION: For bullets only, the structure is different. For a bullet, the type is set to $1868 to indicate that it is the special "smart bomb" and any other non-zero value is the bullets velocity (the first byte is the X-velocity, and the second byte is the Y-velocity). There is a standard control routine for all bullets. The type value is actually a 16-bit relative pointer to the control code. This is the offset from a given address to the control routine. For each kind of object, the file that contains its control code also has a macro to assign these pointers to symbols, which should be used to refer to the object's type: for enemies, 'ENEMIES2.ASM'; for the player's bullets, 'BULLETS.ASM'; and for enemy bullets, 'EBULLETS.ASM'. The offset is defined relative to the main processing routine for each object type, so that it can be accessed with a single jump to a PC-relative indexed address. Previous versions of the game had used double-pointers for this, where the type values are just indexes into a table, and the numerical values were entered into the code. This system is not only smaller and faster, but also much easier to work with; be glad that it changed. Since the type is a 16-bit relative pointer, problems may occur if the program ever becomes larger than 32 kilobytes. In the cases of the objects, all relevant routines are close together, so the size of a specific section would have to reach 32 kilobytes to cause problems. However, 16-bit relative pointers are used in other places as well, over more widely separated areas, a good reason to keep the game small. (Actually, the only such cases are probably the shop, free ship, and end game calls from the level handler; if you really want to make the game huge, you may need to rearrange the code to make those closer together). Now on to the other variables, which should be much simpler. The ?_dmg variable is an amount of damage. For a player bullet or enemy bullet, this holds the amount of damage the object will do. For an enemy ship, this is the amount of damage it can take before being destroyed. Note that the only collisions detected are between player bullets and enemies, enemy bullets and the player, and between the player and scenery or enemies (player to enemy collisions will damage only the player). Collision between enemy bullets and enemies, for example, don't do anything. The next four variables are quite simple. ?_x and ?_y are the coordinates of the upper right corner of the object. ?_w and ?_h are the width and height, respectively. These are used for collision detection so it is important to get them right. To make an enemy be off the screen (e.g. at the beginning of a level before it enters), use a Y coordinate of -300 to assure that the image won't be drawn and no collisions will occur with other objects. The ?_image field is the sprite "handle". This is an offset to the sprite's image, which is used to refer to the sprite for most sprite drawing operations. The sprite is drawn by a separate routine from your control code, so you should not draw it yourself. It is always drawn if the object has a positive Y-coordinate. If you want an object to be invisible, use the in_Fake sprite handle here. The ?_data field provides 2 bytes of storage for any data you want the object to have, such as a status, countdown timer, velocity, etc. This is not used anywhere outside your control routines. EXCEPTION: For bullets, there is a static allocation of this data also. The first byte of it is the counter (used only for the bullets that have varying X-velocities) and the second byte is the type. Type is 0 for normal bullets (ones which move in a straight line), 1 for bullets with varying X-velocities (such as the Quadruple Cannon), and 2 for bullets that change velocity in the Y-direction (arches, where -1 is added to the Y-velocity each frame). For enemies, there is also an e_dstry field, which is a relative pointer to the destruction routines. This code will be called when the enemy is destroyed, and normally changes the type to an explosion, but can also do special things. ______________________________________ Overview of program flow The first step is the "low-level initialization". If the program is a nostub program, this first back up registers, then the LCD. It installs crash protection on all calculators. Then, it calls the main game code. After the game returns, the crash protection is removed, the interrupt mask, frequency, and vectors are restored, and the LCD and registers are restored if it's a nostub program. In the event of a crash, an error message is shown displaying the register contents, as well as the program's start address and the contents of the top of the stack. When the error screen is exited, the code for exiting the game is executed. The second step is to check for a saved game. If one is present, it is restored, and then execution proceeds to the main loop. Otherwise, the next step is to initialize the screen and allocate the data buffer on the stack. After that, the custom interrupt 5 handler, which is used for synchronizing the game, is installed. Then the title screen is displayed. This handles keypresses in the obvious way, and (if not on a TI-89) also calls the regular screen side routines to draw the sides. Once the main loop is approached, the next step is to set the interrupt frequency (again used to keep constant speed) and disable Auto-Int 1 for best performance. After that, the main loop, which handles all of the game-play actions, is entered. These routines are described with brief comments in the 'PHOENIX.ASM' source file, along with detailed descriptions in their own source files and the next section of the document. During gameplay, the main loop executes through and through, mainly just calling the gameplay routines each frame. However, there are some specific flow characteristics that are important: 1) Level loading. When there are no more enemies left, the level loading routine is called. This looks up level from a list. Some levels actually are code, which is run to set up the level (or perform special functions). Other levels are simply lists of which enemy types are present, and how many of each should be created. 2) The shop. The shop is actually entered by the level loader, and really consists of a level which you simply when immediately. However, the code for its loader displays the shop screen. When this screen is shown, Auto-Int 1 is turned back to allow TIOS keyboard reading. It is deactivated again after exiting. 3) Enemies and enemy bullets. Each of these things is processed using a jump table that contains specific routines for each type of item in the array. Refer to the specific source files, and their summaries below, to see how each type of object works. 4) Player bullets. For these, one standard routine goes through all of the bullets and proccesses them. The first word of each object's structure is always its type. This type is an offset into the jump table (0 indicates that that element of the array is unused, and thus no routine should be called; negative numbers have various special meanings). Since the jump table is a list of word offsets, the types are always multiples of 2 so they can be used to directly index the table. 4) Winning the game. If you win the game, your score will be calculated (and, of course, you get a 0 total if you used the cheat key) and you can enter your name if you got a high score. For the high score entry, and display, Auto-Int 1 is switched off to allow keyboard reading. It's turned back on once you exit the high scores, at which point the game simply wraps back to the beginning. ______________________________________ File-by-file description This section contains a brief description of the functions of every file. For more infromation, see the files themselves. Files which contain any code that varies between calculators are identified with (*) after the filename. -------- BULLETS.ASM This file contains code to move and display the player's bullets. Each of these two functions is invoked once per frame in the main loop. -------- COLLIDE.ASM This file contains the true collision detection code; it tests for collisions between any objects by first seeing if their boundaries overlap, and if so then checking if the image data actually hits by drawing the images in a temporary buffer. -------- DISPLAY.ASM (*) This file contains basic display routines. This includes synchronizing the game timing to the interrupt, copying the display buffer to the screen, clearing the display buffer, and setting up the in-game display. -------- EBULLETS.ASM This code handles enemy bullets. It includes both checking for collisions between enemy bullets and the player, as well as the routines to draw enemy bullets on the screen and move them. -------- EDESTROY.ASM This contians the enemy destruction routines; they are called when an enemy is destroyed. Normally, they simply change the enemy to an explosion, but they can do other things such as releasing a new enemy. -------- ENDGAME.ASM (*) This contains the code to calculate your score, prompt for your name if you get a high score, and display the high score table. -------- ENEMIES.ASM This contains code to display enemies on the screen and handle collisions between enemies and the player's bullets. It also includes the skeleton of the routine to move enemies; that is, it runs through the enemy array, and calls enemy control code for each enemy. -------- ENEMIES2.ASM This file contains the actual enemy-specific control code for each type of enemy. -------- ESHOOT.ASM This file contains firing routines which are called from the enemy control code in ENEMIES2.ASM to fire enemy bullets. -------- FREESHIP.ASM (*) This file displays the screen offering the user a free ship. -------- INIT.ASM (*) This file sets up crash protection, which will display an error message and dump regsiter contents if an exception is reached, and then allows the user to exit safely (sometimes). It also restores the timer rate, interrupt mask, and interrupt 5 vector after the program exits. If the program is built in nostub mode, this file also backs up the registers and LCD data. This file calls the main rest of the program as a subroutine. -------- LEVELS.ASM This file contains the code which initializes levels of the game. This includes level-loading code, data for describing levels, and an interpreter for that data. Specifically, initializing a level consists of loading the enemy array, and also setting the pointer to the coordinate table, if one will be used by those enemies. It also contains enemy data blocks (used only within it) that specify types of enemies; in this case, the type data specified control routines, images, strengths, and additional data for the control routine. The shop is actually a special level in which the shop code is called as the loading routine. This level has 0 enemies so you win immediately and progress to the next level. -------- LIB.ASM (*) Contains primitive routines for reading the keyboard, display text, and generating random numbers. -------- MEGABOSS.ASM Contains control code for the megaboss, as well as the enemies it spawns. -------- PHOENIX.ASM (*) This is the main source file. It includes all of the other files, sets up a new game by initializing the game data, and contains the main loop. -------- PHOENIX.H Defines the games variables, as described above and below. -------- PLAYER.ASM (*) Handles movements of the player's ship, displays the player's ship, and tests for collisions with the walls. -------- SAVEGAME.ASM Handles saving and restoring the game. This is done without using any extra memory by hiding the variables on the last part of the stack. -------- SHOOT.ASM (*) Fires the player's weapon. Basically, it checks for pressing the weapon selection and fire buttons, and inserts bullets in the bullet area when you shoot. -------- SHOP.ASM (*) As you can guess, this displays the shop, and allows you to select and purchase the items. -------- SIDES.ASM (*) Scrolls and displays the sides of the screen. -------- SPRITES.ASM Contains the simple "sprite" displaying routine used throughout the game, as well as all sprites in the game in binary format. -------- TITLE.ASM (*) Display the title screen, allowing the user to view various pages of information, and select difficulty level and (on the TI-89) speed. -------- VERSION.H This files contains definitions and macros holding the programs version number which are used elsewhere in the program. These are kept in a separate file so that files containing things like the title screen code won't need to be edited for each new version. This file also contains identification used for recongnizing saved games; if you are modifiying Phoenix, be sure to change it as indicated so other versions won't try to restore your saved games (and vice versa). -------- WHICH.H (*) This identifies the target calculator as "ti89", "ti92" or "ti92plus" by setting that define to 1. This is set before assembling to choose which calculator to build for. ______________________________________ How to create new levels 1) Prepare a level loader. A. Easy Way (table-based) Put a list of enemies for this level in LEVELS.ASM. Under the section with the header "****************** LEVEL LOADERS", put in something that looks like this: Level_Label: LVLDB 2,standard_data_bank LENTRY 10,Enemy_1 LENTRY 10,Enemy_2 The LVLDB line first holds the number of entries, then the data bank of coordinates to use (if you are using only enemies that don't use a data bank, just use "LVL" in place of LVLDB. The next lines are just entries for each type of enemy, first put the number of enemies of the type, then the type name. B. Hard Way (code-based) Write the level loading code in LEVELS.ASM. This code should initialize the number of enemies remaining, the enemy data array, and the coordinate table (if needed). This is very simple if you use the Load_Enemy_Info routine in that file; see the comment above it for details. See the comments for 'Load_Level' at the end of LEVELS.ASM for details on how the level loader should work, and on its initial register values. 2) Add the level to the level table. IThis is done simply by adding an invocation of the LEVEL macro (the format should be clear from the existing entries). This is also in LEVELS.ASM; the table begins a few lines past the header "************************** LEVEL TABLE". If you used method A above, then use 'LDATA' instead of 'LEVEL'. 3) Put this level in the list of levels (which is at the top of LEVELS.ASM) to be played for one or more difficulty levels, in LEVELS.ASM. You do this by simply adding a dc.b directive storing the index byte you gave the level in (2) wherever you want the level to be played. 4) If you are using an enemy that follows a formation, and want to create a new formation for your level, you also will need to create a coordinate data bank. For the standard enemies, the data bank simply contains the X and Y coordinates of each enemy; one word for the X coordinate, then one for the Y coordinate, repeated for each enemy. ______________________________________ How to create new enemies 1) Obtain image(s) for the enemy. You can use images already used for other enemies, or make your own. The images are kept in SPRITES.ASM. The format of the sprites should be clear from looking at the existing ones. If not, there are also some comments about it in SPRITES.ASM. 2) Write the enemy's code. This is the code which is called for the enemy every frame and can move it. This code is kept in ENEMIES2.ASM. The comment for Move_Enemies at the beginning of ENEMIES.ASM describes what these routines should do. Each enemy should be assigned a type symbol, its "name". The code for the enemy should be under the label C_name. At the end of ENEMIES2.ASM, add a called to the ETYPE macro with the name to generate the type. This sets the symbol to the offset of the enemy's code; this symbol is stored in the enemy array. You may, of course, create multiple enemy types for the same enemy; that is, the enemy will change types to change directions or for different phases of its flight. These things can usually also be done with only one type, using the data field to keep track of the enemy's state. Of course, if you want to make a new enemy which behaves like existing ones, but just has things like image and strength changed, this step is not necessary. Note that in the current version of Phoenix, enemies will never collide with the player. The player's drawing routine will detect the collision if the player is actually in contact with the enemy (that is, a pixel-to- pixel collision) and damage the player. However, the enemy will not be damaged by the collision. If you plan to modify the game so that enemies may collide with the player, you should take this into consideration; it might be a good idea to add regular player-to-enemy collision detection like the current collision detection between the player and the enemy bullets. 3) To make the enemy shoot, you can use the enemy firing routines in ESHOOT.ASM. Refer to the comments in that file to see how you use them. Of course, you can also make your own firing routines. If you want to create a new type of enemy bullet, add it to the file EBULLETS.ASM. You can refer to the comments and existing code in that file to see how to add an enemy bullet; the process is essentially the same as steps 1 and 2 of creating and enemy. 4) Add it to the enemy data bank at LEVELS2.ASM. This stores the description of a specific enemy type, including its code offset, its image, and the amount of damage it can take. Entering the enemy here is not completely necessary, but allows it to be loaded with the Load_Enemy_Info routine that greatly simplifies level loaders (if you are using the simpler table-based method of level loading, you must do this as the loader only uses such entries). 5) Create new levels featuring this enemy, or modify existing levels to use this enemy. 6) If you want anything special to happen when the enemy is destroyed, such as a unique explosion, releasing a smaller enemy, or something like that, add the code for that destruction to EDESTROY.ASM, and modify the level loader to install it in the enemy data structure. ______________________________________ Phoenix coding and commenting style This section describes the coding and commenting style currently used in Phoenix. It's mainly here to specify precisely what should be clear from examining the code. The only reason to follow this style is to be consistent with the rest of the program. -------- Code formatting Tabs should be set to 8 spaces, with soft tabs (i.e. a TAB actually puts spaces to bring the cursor to the new position. Mnemonics and most directives begin at the first tab stop (column 9) and arguments at the second tab stop (column 17). An exception to this is the conditional assembly directives, which should start in column 4. Directives (other than dc.x) should be in all capital letters. Instructions should be in all lowercase letters, except for capital letters in symbols that they reference. Lines should be a maximum of 77 characters in length. -------- Block comments Ever major section of the code begins with a block comment describing it. The main block comment consists of asterisk in the first 45 columns, followed by a space and then the description of the section in all capital letters. Additional explanation of the block follows on subsequent lines. Each line of additional commenting begins with on asterisk, a space, and then the text. If any additional comments below the main comment line are present, there should be one line of 8 asterisks to mark the end of it. Multiple paragraphs in this explanation should be separated with lines containing only an asterisk. There should also be one such line at the beginning and at the end. There should always be a blank line before and after a block comment. -------- Individual comments Comments for individual lines begin after a semicolon in column 41, at the fifth tab stop. If necessary to write more than will fit on a line, start the rest at column 49 in a blank line, or better yet, consider moving the explanation to the block comment. -------- Variables All global variables are defined in 'phoenix.h'. Space for them is reserved in the stack by the 'rs' macro used there. Variable names should be all lowercase, using underscores to separate different words. Structure offsets are also defined there, as equates, and should also be lowercase with words separated by underscores. -------- Label names Major labels (e.g. those reference by other sections of the program) should have each word capitalized, and separate words by underscores. All other labels should have words separated by underscores, and all in lowercase. -------- Register use The A5 register should always point to the data are, and A6 should always point to $600000. Other specific sections, such as enemy handlers, have additional requirements. Unlike the requirements above, this one is very important, as these values are depended upon in many places.