---------------------------------------------------------------------- ---------------------------------------------------------------------- BASICVISION - ColecoVision game programming language - DESIGN DOCUMENT Written and updated by Luc Miron - Version 1.1 (2010-03-15) ---------------------------------------------------------------------- ---------------------------------------------------------------------- ================ ================ 1 - INTRODUCTION ================ ================ This text file describes a proposed programming language, titled BasicVision, aimed at the creation of ColecoVision games. It could be modified for other gaming platforms, but this goes beyond the scope of this document. Objectives ---------- - Have a programming language that's easy to learn and fun to use, although not necessarily easy to master for absolute beginners. - Have a language that combines the simplicity of a BASIC-like language with the power of assembly language (at compile-time). More notably, the compiler should be able to translate complex arithmetic equations into assembly language equivalents at compile-time. Certain familiar statements, such as "IF-THEN-ELSE", and "WHILE" should also be supported. - Have a language where the use of the main system stack is non-transparent, which means that the programmer needs to manage the stack himself. For a gaming console with very limited RAM like the ColecoVision, having the programmer manage the stack himself is actually the better option, especially where debugging is concerned. It should be noted however that it is impossible to completely avoid using the stack: For instance, complex arithmetic calculations such as ((x + 2) * (y + 9)) require the use of the stack to store intermediate calculation results. - Have a language that is designed to be used in conjunction with external tools, most notably graphic and sound creation applications. Data created with these tools would be automatically included with the source code at compile-time. - Have a language which is designed to allow the conception of bankswitched games that break the 32K limit of standard ColecoVision cartridges, as well as other non-standard hardware extras such as EEPROM savegame chips. General notes ------------- - Language keywords are case-insensitive. They are presented in uppercase in this document for easier reading. - Variable names are case-sensitive ("aa" is different from "AA" is different from "aA"). Also, variable names cannot begin with a digit character, and the only non-alphanumeric character allowed in a variable name is the underscore (_). - Code indentation is recommended, but not enforced. The compiler's parser ignores spaces and tabs at the beginning of any line of code. This is why such statements as ENDIF, ENDWHILE and ENDLOOP are mandatory. - Memory addresses (in RAM, ROM or VRAM) are represented as regular UINT values (unsigned integers), and the programmer can add and substract offset values from such memory addresses without being hassled by the compiler (unlike pointers in C language, where modern C compilers balk at raw pointer offsets). - Boolean values are defined as zero and non-zero integer values: A conditional expression is TRUE if the result of the processing of such an expression returns a BYTE, UBYTE, INT or UINT value of 0 (zero). The expression is FALSE if the result is anything else than zero. - The decimal (123), hexadecimal (0x0A08) and binary (0b00110010) notations are supported for BYTE, UBYTE, INT and UINT values. - Specifying the name of an array variable without any index between brackets ("data_array" instead of "data_array[n]", for example) is the same as saying "starting memory address of that array variable". - The semi-colon (:) character signals the start of a comment string, so the rest of the line is ignored. A comment may start at the beginning of a line, or may be inserted at the very end of any line of code. - "\" is a line breaker character, signaling that the current statement is continued on the next line of the source code. Any non-space and non-tab character that appears on the line after the line breaker character will be considered illegal by the compiler, unless the next non-space and non-tab character immediately after the line breaker is a ":", which indicates that the rest of the line is a comment string. Current to-do list for this document ------------------------------------ - Add complete list of operators (arithmetic, unary, etc.). - Define the system for the inclusion of sound data. - Define the VDP I/O system (to set the screen mode, etc.). - Define how bankswitching can be implemented within the language. This includes the study of how to distribute compiled code and data in each of the banks. - Study the possibility of adding support for "ASM blocks", which would be SUB blocks written in straight Z80 assembly language. ======================== ======================== 2 - AVAILABLE DATA TYPES ======================== ======================== Below are the available data types usable under this programming language. Take note that floating-point values will be added in a later revision of the language, if the need for it is confirmed. BYTE : Signed single-byte integer value (range: -127 to 128) UBYTE : Unsigned single-byte integer value (range: 0 to 255) INT : Signed two-byte integer value (range: -32767 to 32768) UINT : Unsigned two-byte integer value (range: 0 to 65535) STRING : Alias for one-dimensional UBYTE ARRAY. ARRAY : one-dimensional or two-dimensional table of BYTE, UBYTE, INT, or UINT values STRING ARRAY : Alias for two-dimensional UBYTE ARRAY. Notes: - Arrays are referenced from zero to (length of array - 1). - Arrays can be one-dimensional or two-dimensional only. - Arrays cannot be redimensioned at run-time. - When used with the PRINT command, the contents of a STRING variable defined in RAM must be terminated with a "null" character ("\0"). This rule does not apply to string constants. ================== ================== 3 - PROGRAM BLOCKS ================== ================== The source code is structured in "blocks". There are several types of blocks, namely "RAM", "DATA", "NMI", "GAME LOOP" and "SUB" blocks. --------------- 3.1 - RAM BLOCK --------------- Format: RAM DEFINE AS [AT ] [= ] DEFINE AS STRING [AT ] [SIZE ] [= ""] DEFINE AS ARRAY [AT ] [SIZE [,]] [= ] DEFINE AS STRING ARRAY [AT ] [SIZE ,] [= "",...] ENDRAM The RAM block is used to declare (and initialize) variables, which can be modified at run-time. Only one RAM block can be defined in a project, so unless the project is fairly small, it's usually a good idea to put the RAM block in its own separate source file. If is also recommended to always set a default value for any variable defined in the RAM block, but this is not enforced (unlike DATA blocks where default values are mandatory). All variables defined in the RAM block are global, which mean they are accessible from any point in the program at all times. It should also be noted that it is allowed to define two variables that reference the same memory address in RAM. For example, you can define BYTE and UBYTE variables that point to the same memory address, or you can define a UBYTE variable that points to the second half of a UINT variable. Any variable in the RAM block which has a default value receives this default value only once, at boot/reset time. For arrays and strings, the "SIZE" option is optional. If it is not specified, then the size is determined from the default data after the "=". If there is no default data specified, then the SIZE option is mandatory. The determined size of a string is actually (number of characters + 1) to give room for the hidden terminating null character ("\0") at the end of the string. If the "SIZE" option is specified instead of a default value, the programmer must remember to add that extra byte for the null character. Example: RAM DEFINE var1 AS INT AT 0x7000 = 100 : integer with initial value of 100 DEFINE var2 AS STRING = "hello world" : size is determined from default value (= 12) DEFINE var3 AS STRING ARRAY SIZE 10,3 = \ : 3 strings of 10 characters each "line 1", "line 2", "line 3" : No need to put "\0" at the end of strings DEFINE var4 AS UINT ARRAY SIZE 4,3 = \ : 3 lists of 4 unsigned integers 1000 1004 1107 1991, 12 3000 540 45, \ 23 1120 2 0 ENDRAM ---------------- 3.2 - DATA BLOCK ---------------- Format: DATA CONST = DEFINE AS [AT ] = DEFINE AS STRING [AT ] SIZE = "" DEFINE AS ARRAY [AT ] SIZE [,] = , , ... DEFINE AS STRING ARRAY [AT ] SIZE , = "", "", ... ENDDATA DATA blocks serve the same general purpose as the RAM block, but DATA blocks are used to define read-only static values which are stored inside the cartridge and cannot be modified at run-time. Unlike the RAM block, the programmer can define several DATA blocks, as many as he wants within his project. The constants declared in a DATA block use the same data types as the RAM block, but it is mandatory that each constant has a declared value (obviously). Any constant not initialized with a value will be marked as illegal by the compiler. Since there may be multiple DATA blocks defined in a project, it is possible to define constants with the same name in multiple blocks. While it is not recommended to do this, the compiler supports it as long as the programmer prefixes the constant name with the block name (for example: datablock1.var1, datablock2.var1, datablock3.var1). CONST is used to define "virtual" constants, which is essentially the same as the "#define" command in C language. When such virtual constants are encountered anywhere in the source code, the BasicVision compiler replaces the constant's name with the constant's value before generating the assembly source code. Virtual constants can only be declared in DATA blocks, and a single virtual constant cannot be defined more than once in an entire project. For arrays and strings, the "SIZE" option is optional. If it is not specified, then the size is determined from the default data after the "=". The determined size of a string is actually (number of characters + 1) to give room for the hidden terminating null character ("\0") at the end of the string. Example: DATA datablock1 CONST option_menu_length = 3 : Defines a virtual constant DEFINE var1 AS UINT = 0xFF00 : Hexadecimal notation is used for setting the value DEFINE var2 AS BYTE ARRAY = \ : Array of 10 bytes 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ENDDATA --------------- 3.3 - NMI BLOCK --------------- Format: NMI DEFINE AS ... ... ... ... ... ENDNMI The NMI block is called once upon every non-maskable interrupt generated by the ColecoVision's graphic chip (this happens roughly 60 times per second). There can only be one NMI function defined within a project. Having no NMI block defined in a project is permitted. The programmer should consider the NMI block as a process that runs concurrently to the active GAME LOOP block: When the graphic chip generates an interrupt, the GAME LOOP block is suspended (no matter what part of the block is current being executed) and the NMI block is executed. Once the NMI block is done, control returns to the GAME LOOP block, which resumes at the point where it was interrupted. In general, the programmer should code his game so that the end of the GAME LOOP block is reached before the next graphic chip interrupt is generated. This helps in making the game run smoothly. Also, the NMI block should only be used for small timing-sensitive things, such as updating sound output, monitoring the current state of connected controllers, and incrementing in-game timers. All other processing should be done in GAME LOOP blocks, and calling non-trivial SUB blocks from the NMI block should be avoided at all costs (although it is technically permitted). As with all code blocks, all local variables must be defined (with the DEFINE statement) at the very beginning of the block. Example: (To be added later) -------------------- 3.4 - GAMELOOP BLOCK -------------------- Format: GAMELOOP DEFINE AS ... INIT ... ... ... ... ENDINIT ... ... ... ... ENDGAMELOOP The GAME LOOP block, as the name indicates, is a block that runs in an endless loop. When control is given to a GAME LOOP block: 1) The statements inside the INIT sub-block are executed only once. 2) The rest of the GAME LOOP block is executed. 3) When the end of the GAME LOOP block is reached, the program waits for the ColecoVision's graphic chip to generate a non-maskable interrupt. 4) The NMI block is executed (see NMI block documentation above for more details). 5) The execution of the GAME LOOP block is restarted at the first statement immediately below the ENDINIT line. There can be multiple GAME LOOP blocks defined in a program, but only one such block can be active at a time. The programmer can jump from one GAME LOOP block to another via the JUMPTO command. In fact, since a GAME LOOP block loops endlessly on itself, the only way to break out of this endless loop is to jump to another GAME LOOP block. The INIT sub-block is optional, and should be used to do certain things that need to be done only once for this GAME LOOP block, like loading graphic data in VRAM for example. Note that the INIT portion of a GAME LOOP block is executed each time a JUMPTO command gives control to this GAME LOOP block. Having multiple GAME LOOP blocks is generally good practice. For example, a GAME LOOP block can be defined for the title screen, another for the main game, another for the game over screen, etc. It should be noted that if the ColecoVision graphic chip generates an interrupt before the end of the GAME LOOP block has been reached (in other words, if the GAME LOOP block takes too long to do its work) then execution of the GAME LOOP block will be suspended, the NMI block will be executed, and then the GAME LOOP block will resume its execution from the point it was interrupted. This kind of situation can possibly lead to bizarre behavior at run-time, so it should be avoided as much as possible. In essence, the code inside any GAME LOOP block, as well as the code inside SUB blocks that are called by GAME LOOP blocks, should be optimized as much as possible. As with all code blocks, all local variables must be defined (with the DEFINE statement) at the very beginning of the block. Example: (To be added later) --------------- 3.5 - SUB BLOCK --------------- Format: SUB DEFINE AS ... ... ...... ... ENDSUB SUB blocks are essentially user-defined functions. They are called from other blocks via the GOSUB or POPSUB statements. The programmer can do anything he wants in SUB blocks, except using the JUMPTO command. When the last statement of a SUB block has been executed, control is given back to the block which called this SUB block. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ COMMENT: Why is the JUMPTO command not allowed in a SUB block? Bear with me as I try to explain the problem. Let's say a GAME LOOP block calls a SUB block, and this SUB block calls another SUB block, which itself calls another SUB block. When a SUB block is done, it returns control to the calling block, and this implies that the address of the next instruction in the calling block must be stored on the main stack each time a SUB block is called. There's no way around this technicality. Now let's say that the last SUB block (in the calling sequence described above) performs a JUMPTO to another GAME LOOP block. Doesn't this mean that the previous GAME LOOP block is now defunct, and that whatever the previous GAME LOOP block was doing becomes irrelevant? This implies that the sequence of SUB block calls is no longer valid, which leaves us with defunct "back addresses" stored on the stack! Cleaning up these stacked addresses (as well as other junk possibly left on the stack by the previous GAME LOOP block) could be a serious pain, so it's better to avoid the problem altogether by reserving the use of the JUMPTO command to the GAME LOOP blocks themselves. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUB blocks are not designed to return anything, so if a SUB block needs to return any kind of result, it can be done via a given global variable (defined in the RAM block) or via the main stack, with the use of a PUSH command in the SUB block and a POP command in the calling block. Passing parameter values to a SUB block is usually done via the main stack, and the programmer needs to POP the individual values from the stack before doing anything else. See the GOSUB and POPSUB statements to learn how to push values onto the stack. As with all code blocks, all local variables must be defined (with the DEFINE statement) at the very beginning of the block. Example: : This SUB block puts the absolute value of a given INT on the stack. : The original INT value is placed on the stack before calling this SUB block. SUB abs DEFINE val AS INT POP val IF val < 0 THEN PUSH -val ELSE PUSH val ENDIF ENDSUB ============== ============== 4 - STATEMENTS ============== ============== ---------------------------------------------- 4.1 - DEFINE (in NMI, GAMELOOP and SUB blocks) ---------------------------------------------- Format: DEFINE AS [LINKTO ] The DEFINE statement is used in the RAM block to define global variables (which are visible by all other blocks in the project), and also in DATA, NMI, GAME LOOP and SUB blocks to define local variables. Both local and global variables are the same in the sense that they are both defined in RAM, the only difference is that a local variable is only visible within the block where the variable is defined. So an "xyz" variable defined in one code block is distinct from an "xyz" variable defined in another code block, and these two "xyz" variables are linked to separate addresses in RAM. In order to save up on RAM usage, it is possible to link a local variable (defined in any code block) to a global variable in the RAM block, via the "LINKTO" option. When "LINKTO" is used, the local variable becomes a simple alias for the global variable. The datatype of the linked local variable (specified with the "AT" option) must loosely match the datatype of the global variable: Only a BYTE/UBYTE local variable can be linked to a BYTE/UBYTE global variable, and only an INT/UINT local variable can be linked to an INT/UINT global variable. In the same fashion, the datatype of an ARRAY must match between local and global arrays. It is permitted to have several local variables linked to the same global variable. The programmer must be careful when defining such aliases, especially within SUB blocks, because the value of a local alias may be overwritten by another SUB block which uses the same global variable for its own alias. Example: DEFINE pos_x AS BYTE = 100 DEFINE color_spr AS INT LINKTO color_1 : 'color_spr' is an alias of 'color_1' in RAM block --------- 4.2 - LET --------- Format: LET = The LET statement performs the assignment of a value to a variable. The variable must be defined in the project's RAM block, and the datatype of the expression (after the "=" character) must match the datatype of the variable specified. Take note that the evaluation of the expression may imply the use of the main stack to store intermediary calculation results. Examples: LET var1 = 45 + 2 - x : "var1" and "x" are UBYTE variables LET var2[4] = 0xFF0A : "var2" is a UINT ARRAY variable LET var3 = "hello" : "var3" is a STRING or a UBYTE ARRAY ---------- 4.3 - tags ---------- Format: # Tags are jump points referenced by GOTO statements. See the documentation of the GOTO statement for more details. Nothing else but a tag name is allowed on a line that begins with "#". Also, tag names are case-sensitive, just like regular variable names. Example: #start_of_block ---------- 4.4 - GOTO ---------- Format: GOTO When a program encounters a GOTO statement, the next statement that will be executed will be the one immediately following the tag specified, which must be located inside the current block. It is illegal to use a GOTO statement to jump to a tag located outside of the current block. The programmer can include the "#" prefix with the tag name if he wants, but it's optional. Note that placing a GOTO statement at the end of a GAME LOOP block (to restart the loop) is unnecessary, because the looping is done by default. Example: GOTO #start_of_block : The "#" character is optional ----------- 4.5 - GOSUB ----------- Format: GOSUB [WITH [...]] Causes the program to jump to the beginning of the specified SUB block. Once the SUB block has completed its job, control will be returned to the calling block, so the next statement to be executed will be the statement which immediately follows the GOSUB statement. The "WITH" option lets the programmer push values which should normally act as parameters for the SUB block called with GOSUB. So for example "GOSUB xxx WITH a b c" is the same as "PUSH a, PUSH b, PUSH c, GOSUB xxx". Note that it is not permitted to push arrays onto the stack using the "WITH" option. Example: GOSUB UpdateSprites GOSUB Calculate WITH 3 (x+1) : Values (3) and (x+1) are pushed onto the stack ------------ 4.6 - POPSUB ------------ Format POPSUB AS [BYTE|UBYTE|INT|UINT] [WITH [...]] POPSUB is essentially the same as GOSUB, except that it is designed to be used within an arithmetic expression. More precisely, the SUB block called with POPSUB is expected to place a single value onto the main system stack before it exits. The expected datatype of this returned value is specified with the mandatory "AS" option. The "WITH" option works the same way as with GOSUB, to push parameter values onto the stack before calling the SUB block. Example: LET val = POPSUB getRandomValue AS BYTE WITH 9 ------------ 4.7 - JUMPTO ------------ Format: JUMPTO The JUMPTO statement is used to give control of the execution to another GAME LOOP block. This statement can only be used inside a GAME LOOP block. It cannot be used inside a SUB block or inside the NMI block. Important note: When a JUMPTO statement is executed, the program will wait for the next NMI interrupt before handing over control to the specified GAME LOOP block. Example: JUMPTO TitleScreenLoop : 'TitleScreenLoop' is the name of a GAME LOOP block ---------- 4.8 - PUSH ---------- Format: PUSH ||| The PUSH statement is used to place a value on the top of the main system stack. Only integers (INT and UINT) and bytes (BYTE and UBYTE) can be pushed. BYTE and UBYTE values are pushed as INT values with the upper 8 bits set to zero. Arrays cannot be pushed all at once, but individual values inside arrays can be pushed. Examples: PUSH result_value : result_value can be INT, UINT, BYTE or UBYTE PUSH list[4] : pushing a single value within an array --------- 4.9 - POP --------- Format: POP ||| The POP statement is used to take one value from the top of the main system stack. The value can be an INT, UINT, BYTE or UBYTE, and the expected datatype is dictated by the data type of the variable specified in the POP statement. If the data type is BYTE or UBYTE, the upper 8 bits of the popped value will be lost. The variable specified with the POP statement must be defined in the project's RAM block. Arrays cannot be popped from the main stack, but individual values can be popped and placed inside arrays, one value at a time. Example: POP byte_value POP list[0] POP list[x] : array offset specified via variable 'x' ----------- 4.10 - PRINT ----------- Format: PRINT [USING ] AT , PRINT TILE AT , The PRINT statement is meant to be a powerful yet easy-to-use tool for printing text on the screen. While the WRITEVRAM statement can perform the same job, PRINT lets the programmer put readable ASCII-based text in his source code, offers a way to encode this ASCII-based text into a non-ASCII character set automatically (via the optional "USING" option) and also lets the programmer specify the line/column coordinates where the text needs to be displayed on the screen via the mandatory "AT" option. For the "AT" option, the line number must be an integer value between 0 and 23, and the column number must be an integer value between 0 and 31. If the string of text goes beyond the right edge of the screen, it will spill over to the beginning of the next line on the screen. If the string of text goes beyond the bottom-right corner of the screen, it may corrupt VRAM and cause problems, so the programmer must be careful not to let that happen. The string expression specified can be a concatenation of sub-strings and integer values, assembled with the "+" operator. Integer values are converted to strings automatically as they are encountered in the string expression. For example, if the variable 'x' contains the integer value 99, the statement [PRINT "HELLO MISTER-" + x AT 0,0] will display "HELLO MISTER-99" at the top left corner of the screen. To use the "USING" option, the programmer must first define an array of UBYTE values, then specify the name of this array as parameter of "USING". When using PRINT with regular strings, the array must be 256 bytes in length, and each value in this array is the "alternate" tile number of the ASCII character to print. For example, if "charset" is the name of the array, then if charset[65] contains the value 44, it means that when the letter "A" (which is character #65 in regular ASCII encoding) is printed, the actual character displayed on the screen will be tile #44 (see full example below). If the string expression to print is a simple string constant, the tile reference conversion is done at compile-time and the array itself will not be included in the generated assembly source code, otherwise the conversion is done at run-time, which is more CPU-intensive. If PRINT is used *only* to print a single integer value, then the array to use with "USING" must be 10 bytes in length, and each value is this array is the tile number of the corresponding digit. For example, if "charset" is the name of the array, then if charset[0] contains the value 44 and charset[1] contains the value 45, then PRINT will display the integer 100 as tile #45 followed by two tiles #44. If the "USING" option is not used at all, regular ASCII character encoding is assumed by default. Any string constant used with PRINT does not need to have a null character ("\0") at the end, because the compiler will add this null character at compile time. However, string variables (in the string expression given to PRINT) need to have the terminating null character included. The "TILE" option can be used to print a single tile specified by its pattern number as an integer value. This is useful when the tile to display is derived from some kind of arithmetic calculation. Note that "USING" has no effect on tiles printed with the "TILE" option. Also, the "TILE" option can be used with other strings via concatenation. If is also possible to specify an actual tile number in a string constant. To do this, the tile number must be indicated as a three-digit code prefixed with "\t". See example below. Example: PRINT "SCORE:" + p1_score AT 0,5 : if p1_score=100, displays "SCORE:100" at line=0, column=5 PRINT "AAA" USING charset AT 1,1 : if charset[65] = 44, then prints tile #44 three times. PRINT "\t005\t141" AT 1,1 : prints tile #5 and #141, no matter what they may be in VRAM. PRINT "STATUS:" + TILE 141 AT 5,5 : concatenates "STATUS:" and tile #141 at line=5, column=5 --------- 4.11 - IF --------- Formats: IF THEN IF THEN ... <...statements...> ... ENDIF IF THEN ... <...statements...> ... ELSE ... <...statements...> ... ENDIF IF THEN ... <...statements...> ... ELSEIF THEN ... <...statements...> ... [ELSEIF THEN ... <...statements...> ...] [ELSE ... <...statements...> ...] ENDIF The IF statement is used to evaluate a condition before executing one or more statements. If there is a statement after the "THEN" keyword (on the same line) then the "IF" statement is assumed to be a "single-statement IF", and this single statement will be executed if and only if the conditional expression evaluates to zero (which means TRUE). In this case, the following lines of the source code are not attached in any way to that "IF" statement. On the other hand, if there is nothing specified on the line after the "THEN" keyword, then all the following statements between the IF and ENDIF statements (or between IF and ELSE, if applicable) are linked to this IF statement, and those following statements will be executed in sequence if and only if the conditional expression evaluates to zero. The ELSE keyword indicates that the following statements (between "ELSE and ENDIF") are to be executed if the conditional expression evaluates to a non-zero result (which means FALSE). The ELSEIF sub-section ("ELSE IF" is also permitted) of an IF statement will be processed only if the initial conditional expression evaluates to FALSE. There can be multiple ELSEIF sub-sections defined inside the same IF statement, and those are processed in sequence until a conditional expression (among those ELSEIF sub-sections) evaluates to zero. An ELSE sub-section placed at the very end will be executed if all conditional expressions (of the IF and ELSEIF) evaluate to non-zero. Note that the main system stack will likely be used during the process of evaluating a conditional expression. Also, it is not possible to use ELSE or ELSEIF with a single-statement IF. Example: IF (POPSUB TestProjectileCollision = 0) THEN LET score = score + 100 (more examples to be added later) ------------ 4.12 - WHILE ------------ Format: WHILE ... ...... ... ENDWHILE The WHILE statement is used to execute a certain number of statements until a conditional expression evaluates to a non-zero value (which means FALSE). These statements are placed between WHILE and ENDWHILE. Note that it is possible to interrupt the execution of these statements and to immediately reevaluate the conditional expression by using the CONTINUE statement. Example: LET line = 23 WHILE line > 0 PRINT "HELLO WORLD!" AT line,11 LET line = line - 1 WAIT 60 : Wait 1 second between prints ENDWHILE --------------- 4.13 - CONTINUE --------------- Format: CONTINUE The CONTINUE statement is used to skip the remaining statements within a WHILE block, and to reevaluate the conditional expression of that WHILE block. If there are multiple WHILE blocks imbricated into each other, then the CONTINUE statement will affect the inner-most active WHILE block. Example: (To be added later) ------------- 4.14 - RETURN ------------- Format: RETURN This statement is used to exit a SUB block before reaching the actual end of the block. It also works in the NMI block. Using RETURN in a GAME LOOP block is illegal. Caution should be observed when pushing and popping values from the main system stack: The RETURN statement should be placed after the PUSH statements, and all PUSH statements should be accounted for upon each use of the RETURN statement. Failure to verify this may lead to stack-related errors, which can be hard to track down afterwards. Example: (to be added later) ----------- 4.15 - LOOP ----------- Formats: LOOP FROM TO [STOPWHEN ] ... ...... ... ENDLOOP LOOP FROM DOWNTO [STOPWHEN ] ... ...... ... ENDLOOP LOOP FOR [STOPWHEN ] ... ...... ... ENDLOOP The LOOP statement is used to execute a certain number of statements a finite number of times. These statements are placed between LOOP and ENDLOOP. There are two available variants to the LOOP statement, namely "FROM-TO/DOWNTO", and "FOR". With the "FROM" variant, the variable used for counting is initialized with the numeric value specified after the "FROM" keyword. If the "TO" keyword is used, the counter will be incremented by 1 upon each iteration of the loop, and these iterations will continue until the counter reaches the value specified after the "TO" keyword. If the "DOWNTO" keyword is used, the counter will be decremented by 1 upon each iteration of the loop, and these iterations will continue until the counter reaches the value specified after the "DOWNTO" keyword. With the "FOR" variant, the variable used for counting always starts at zero, and will be incremented by 1 upon each iteration. These iterations will continue until the counter reaches (n-1), where "n" is the value specified after the "FOR" keyword. So in effect, "LOOP i FOR n" is the same as "LOOP i FROM 0 TO (n-1)". This variant of the LOOP statement is useful for processing arrays, since array indexing always starts at zero, and the length of the array can be given as the limit (after the "FOR" keyword). All variants of the LOOP statement support the optional STOPWHEN option. As the name implies, this option causes the loop to be completely interrupted when the conditional expression returns a zero value (which means TRUE). The specified variable used for counting must be defined in the RAM block, and must be a BYTE, UBYTE, INT or UINT. This variable is not protected in any way, which means that the programmer can change its value at any time within the loop. Of course, such a practice is not recommended, and the WHILE statement may be better suited for such a situation. Examples: LOOP line FROM 23 DOWNTO 0 PRINT "HELLO WORLD!" AT line,11 WAIT 60 : Wait 1 second between prints ENDLOOP LOOP i FOR (LENGTH arr) LET arr[i] = 0 : Initialize an entire array with zeros ENDLOOP --------------- 4.16 - COPYVRAM --------------- Format: COPYVRAM [] Copies one or more bytes to VRAM, starting at a given address (in RAM or ROM space). The last parameter (the number of bytes) is optional only if the first parameter (the start address of the source data) is an unindexed array variable name. In this case, the number of bytes to copy is assumed to be the total length of the array by default. COPYVRAM is useful for loading pattern tables, color tables, etc. in VRAM. However, it can only copy raw (uncompressed) binary data. To uncompress data into VRAM (like run-length encoded data, for example) the programmer must define his own SUB function. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ COMMENT: It goes without saying that a library of useful SUB functions will be offered on the official web site of the BasicVision project, and the function to decode RLE data will be among the first to be included in this library. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reminder: Specifying the name of an array variable without any index between brackets ("data_array" instead of "data_array[n]", for example) is the same as saying "starting memory address of the array". Example: COPYVRAM data_array 0x2000 400 : Copies 400 bytes of data_array at location 2000h in VRAM. --------------- 4.17 - READVRAM --------------- Format: READVRAM Reads one or more bytes from VRAM, starting at a given VRAM address, and put this data at the specified address in RAM. Reminder: Specifying the name of an array variable without any index between brackets ("data_array" instead of "data_array[n]", for example) is the same as saying "starting memory address of that array variable". Example: READVRAM 0x2000 data_array 30 : Moves 30 bytes from 2000h in VRAM to "data_array" in RAM. ---------------- 4.18 - WRITEVRAM ---------------- Format: WRITEVRAM Writes a single UBYTE value in VRAM a certain number of times, starting at the specified VRAM address. It is permitted to use a BYTE value, but this BYTE value will be treated at a UBYTE by WRITEVRAM. Example: WRITEVRAM 0 0x0000 16384 : Reinitializes VRAM entirely by filling it with zeros. --------------- 4.19 - LOADNAME --------------- Format: LOADNAME <2d_array_of_ubytes_variable_name> AT , LOADNAME is a powerful tool used to copy a two-dimensional list of tile numbers (as BYTE or UBYTE values) in the name table in VRAM. The array can be defined in the RAM or ROM space. The line and column numbers describe the coordinate position of the top-left tile of the area. The line number must be a BYTE value between -127 and 23, and the column number must be a BYTE value between -127 and 31. If the line number is greater than 23, or if the column number is greater than 31, then the name table remains unmodified. The array of source data must be two-dimensional, and the area of the screen affected corresponds to the array's dimensions. LOADNAME only supports raw (uncompressed) data. To uncompress tile data into VRAM (like run-length encoded tile data, for example) the programmer must define his own SUB function. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ COMMENT: It goes without saying that a library of useful SUB functions will be offered on the official web site of the BasicVision project, and the function to decode RLE data will be among the first to be included in this library. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the area of tiles defined in the array goes beyond the right edge (or bottom edge) of the screen, the area of tiles is clipped automatically to avoid display problems. The area is also clipped if the line and/or column number is a negative value (which implies that the top-left corner of the area is located outside the screen). Note that a BYTE array is treated as a UBYTE array by LOADNAME. Example: LOADNAME title_logo AT 4,5 : "title_logo" is a 2-dimensional array of tile numbers ----------- 4.20 - WAIT ----------- Format: WAIT The WAIT statement, as the name implies, causes the program to wait until the specified number of interrupts has been generated by the ColecoVision graphic chip. Note that this does not prevent the NMI block from being called upon each interrupt, while the WAIT statement is in effect. Example: WAIT 60 : Wait 60 interrupts, which is roughly equivalent to 1 second. ------------- 4.21 - UPDATE ------------- Format: UPDATE [JOY1|JOY2|JOY1 JOY2] The UPDATE statement is used to update the global status of the joystick input variables. It is usually used within the NMI block, and it updates the values linked to these reserved keywords: - JOY1_DIRECTION - JOY1_FIRE_LEFT - JOY1_FIRE_RIGHT - JOY1_KEYPAD - JOY2_DIRECTION - JOY2_FIRE_LEFT - JOY2_FIRE_RIGHT - JOY2_KEYPAD Examples: UPDATE JOY1 : Update values for player 1's controller only UPDATE JOY2 : Update values for player 2's controller only UPDATE JOY1 JOY2 : Update values for both controllers ------------ 4.22 - START ------------ Format: START Specifies which GAME LOOP block to execute when the cartridge software boots or resets. This is the only command that lies outside of any block, and it can only appear once in any given project. Example: START TitleScreenLoop : "TitleScreenLoop" is the name of a GAME LOOP block. ================================ ================================ 5 - FUNCTIONS AND OTHER KEYWORDS ================================ ================================ ------------ 5.1 - LENGTH ------------ Format: [INNER] LENGTH The LENGTH function evaluates the size of an array at compile-time, which means that the size of the array will become a UINT constant after code compilation. For a two-dimentional array, use "INNER" to determine the first of two sizes (using "LENGTH" without "INNER" will always return the second size value). Example: LET a = LENGTH arr : Variable "a" must be a UINT; variable "arr" must be an array. --------- 5.2 - ABS --------- Format: ABS The ABS function takes a signed value (BYTE or INT) and returns the absolute (unsigned) value (UBYTE or UINT). Example: LET a = ABS b ==================== ==================== 6 - DOCUMENT HISTORY ==================== ==================== * Version 1.0 - February 21st 2010 : - First version of document. * Version 1.1 - March 15th 2010 : - Changed hexadecimal notation from 0####h to 0x#### - Changed binary notation from 0########b to 0b######## - Removed null characters ("\0") in all string constants in the entire document - Removed the INIT statement for initializing arrays (now encapsulated in DEFINE statement). - The SIZE option of arrays and strings is now optional (determined from INIT statement) - Added support for defining local variables inside SUB, GAMELOOP and NMI blocks - Improved the "USING" option of the PRINT statement (for printing single integer values) - Added support for printing tiles by numbers with the PRINT statement (using the "\t" format) - Added "TILE" option to the PRINT statement - Added the STOPWHEN option to the LOOP statement - Added the RETURN statement (usable in SUB and NMI blocks) - Added the "WITH" option to the GOSUB statement - Added the joystick input system - Other minor fixes and adjustments