Part 6: Demos and Prototypes
My apologies for the long hiatus... I've had some interruptions in real life for my hobby work. In particular, I switched jobs, and I moved to a new apartment. On the plus side, I now know that MESS runs pretty well on Vista. (Guess where I'm working?)
I've switched gears a bit with the project, and am now focusing on creating the actual engine prototype instead of modeling small elements. This means there's some serious debugging sessions going on, but it also means I should have executable stuff to look for every new article.
So, let's have a look at the implementation of my demo prototype.
Demo
Screenshots and graphics are nice to look at, but having an actual working game engine would be even better. Unfortunately, this isn't a simple matter.
Because a CRPG is so file-dependent, I must have data for it to load and interact with that is identical or closely matches the final game data. So I can't follow a classic "engine first, content later" methodology, because it has to have something to go on.
At the same time, I can't design all the content first. Especially on a vintage system! It may turn out that the engine is unable to meet the needs of the content, which would require a great deal of revision.
So, what's the solution? Well, design a smaller, much smaller, prototype of the game. At the same time, this can also serve as a demo for the game, something to distribute and give people an idea of what it will be like to play it before the final version is completed.
My reasoning here is:
- Writing the demo is the same as writing the game... most of the game's engine will be completed in the process.
- Demo versions usually have a very small amount of the planned content, which is useful for testing purposes.
- Demos typically include a wide variety of content to sell the game, which is also useful for testing purposes.
- A demo can be used to try out new ideas without as much overhead and revision as a full version.
The demo is, essentially, the beginning of the full game, around 5% of the complete content. This introduces a prospective player to the game, lets them play up to a point and then requires the full version to continue. An import utility would "import" your demo game, so someone can can pick up where they left off when they have the final version.
Some things that the demo won't test or do:
- Advanced file management. Since the demo will be contained on a single 180k disk, it will only need to refer to a single path for all files, without swapping.
- Character creation. The demo will come with a stock party; no character creation is possible.
- Ship sailing, which isn't in the scope of the demo's contents.
- Advanced combat, using high-end spells and equipment.
- Slanting maps. The demo content doesn't have these.
I've decided to make all my work on the demo available for download; you can find them on this page. Aside from keeping everyone's interest, I also think this is a good way to get some external testing. Try it on your various TI configurations! See if you get the "OPERATIONS COMPLETE!" message, or if something strange happens. Let me know if something doesn't seem to be working. I've done my best to ensure this runs on a normal TI, so hopefully there won't be too much difficulty.
Source Control
All programming projects, regardless of complexity or platform, should have some form of source control.
I've often noticed (and been guilty of it as well) that many inexperienced technical professionals usually think that vintage programming is "easy". It reminds me of the English Literature professors who think they could knock out a best-selling novel in a week, if they chose to do so.
The truth of the matter, though, is that any given project is difficult, regardless of the platform. This is especially true in software development. Vintage software may have been written by smaller development teams, but the work was not any easier! In fact, it was often more difficult.
One problem is the general unreliability of older hardware. Nostalgia tends to cloud our memories; we forget, for example, how HOT a vintage computer gets. I remember that I would stop using my TI simply because doing so warmed up my room too much on a hot summer day. And you sometimes wondered if you could keep your coffee cup warm on the cartridge slot panel. There was also the occasional crash, what is called the "blue screen of death" on modern systems, usually caused by a cartridge not inserted quite right, jogging the PE Box cable by accident, or even just an inexplicable momentary fault. Several hours of work could be lost in a single bad moment, or even files damaged if something occured during a read/write operation. This is one reason I develop in emulation.
So, as part of my source control for my CRPG, I took a few steps:
- I have multiple seperate files for different program parts.
This is pretty much a given with TI assembly; it would not be possible on a standard machine to have the entire source code loaded at once.
- All source files are written on my PC, and then "pasted" into the TI's editor running in MESS.
If you turn MESS throttling off, it's usually pretty quick at this, although it occasionally throws in a spare blank line, and if you don't turn off caps lock it will capitalize everything. Keeping track of source is much easier on the PC, and I created my own custom syntax with TextPad so it would colorize and tabulate TI assembly files appropriately.
- I use a seperate 90k disk for each individual file, copying them to a larger disk prior to compile.
This is good for logistics, but it's also to defeat the "MESS file issue", which I have encountered at times. The problem is inconsistent and, to my knowledge, exists in the latest version of MESS. If you have a file on disk that gets bigger, enough to take up another sector, the disk controller emulation does not always update the sector count. As a result, your files "lose" data. In addition, that same lost data can show up in another file as it clumsily attempts to re-order files on disk. You can counter the problem by "wiping" a disk and copying the files sequentially using a disk copy program. I'm not certain if this issue is completely MESS's fault... I haven't really tried a lot of messing about with files in this manner on a TI, and I haven't examined to see how TI's FDR handles a case where a file is now larger than can be fit in the area it's in, because other files buffet it on either side.
- I compile the final object code on a seperate disk and copy it to a "play disk".
This step is again to preserve the file from corruption caused by file sector mismanagement in MESS.
For the conversion of the tagged object file into a series of memory-image files, I use the original SAVE utility provided with the Editor/Assembler cartridge.
There's a couple different programs available for this, including Art Green's RAG Linker, which actually handles the operation much more smoothly. I noticed, though, that it broke a file that was smaller than 8 kilobytes into two seperate files for reasons unknown. So I'm sticking with the original for now.
Debugging
Unfortuantely, the one thing really lacking with vintage programming is solid debugging and diagnostic tools.
Most of the software developed by Texas Instruments was, ironically, not developed on the TI-99/4a directly, but on their 990 mini-computer. It had the same processor but it actually was capable, thanks to the DX10 operating system, of running multiple programs at once. So many TI developers would have their software running in tandem with debuggers or memory editors. Unfortuntately, a real-time debugger on a TI-99/4a is nearly impossible.
The Editor/Assembler does have a debugger. I have not tried to use it, but I've read about other people's experiences. The problem is that it must load the debugger with your program, which means a very large program would not work with it. It also involves some guesswork to identify where the problem is; like most old machines, the TI doesn't recover from a crash situation very well.
So how to debug without a good debugger?
Well, probably the absolute first and best is code review. This involves reading over your source code, following along with what's happening, and looking for any problems or oddities. This technique is still used even with modern programming, albeit in a smaller-scale fashion. (You can be sure no one code reviewed the whole of Vista, which is several million lines of code.)
The main problem with code review, though, is the same problem that writers face; personal bias. More often than not, your eye can skip over code that would break but you automatically assume it would "work fine". Like writers, shelving your work for a decent period of time before picking it up again can mitigate the problem; you have a fresher perspective of the code and are more likely to spot problems. But probably the best method is to have someone else do the code review.
The second method is available to us with one emulator; the very useful on-screen debugger provided with PC99. It's a very nice way to be able to view the contents of memory, registers, and other elements while a TI program is running, and even set up breakpoints based on expected values.
The downsides are:
- PC99 is a commercial emulator, not freeware. You can find it available for purchase here.
- It runs in full-screen DOS mode. Fortunately, it still works with Windows XP. I haven't tested it in Vista yet. This relates to a change in the Windows OS since XP, so it probably will do the same thing there. (Or fail entirely...)
- The assembly code that is shown when stepping through a program is 100% unrolled code, which means every iteration of a loop will be shown. This isn't a bad thing, debugging-wise, but you can sometimes get thrown off the track of where you're at in your code.
The final method is the way I usually have to do it; code in my own artifical breakpoints into the code, along with memory displays as needed. This is a very sloppy and time-consuming method, since it means you have to re-compile your source in entirety. However, sometimes it's the only way to find out what exactly is going on in the environment.
If a particular piece of code seems to be the source of problems, you can break it out into a "model" program that is quicker and easy to compile and edit to find the problems. If you fix it in the model, then hopefully it will work now with the main program.
I had quite a few bugs to hunt in my initial source code, which took me some time to plow through. Probably the worst was the trouble with DSRLNK. I had thought that this routine preserved the PAB address located in the scratch-pad, and I had written my code on this assumption. It turns out, though, that it doesn't! Fortunately, since I was using a custom DSRLNK, I was able to add a few lines to do just what I needed.
Most of the work in my verion 0.15 demo (now available for download) is being done in three files:
The INIT_S file is the start of the program, where it sets up all things for the first and only time. For example, this is where the video mode is set to enhanced graphics mode.
Also done in this section is the loading of the first game file, called PBD. This file contains all the file information for the game, such as where particular files are located in terms of disk and disk drive, and whether a swap needs to be prompted for prior to access. This file will be custom-edited in the final version through the use of a configuration program. For now, any changes needed are done directly using a hex-editor.
The first file loaded is interesting because the first major hurdle here is, where to load the file from? I could have built in a bias for DSK1, the first floppy drive. However, this would make the game break completely if you attempted to run it from a RAMdisk or hard drive configuration.
So the first thing it does it call the disk controller and find out what the last accessed device was, which is where the program initially loaded from. It then constructs the PAB from that information:
DSRADR EQU >8356
CRULST EQU >83D0
SAVADD EQU >83D2
B4 BYTE 4
PBDPAB DATA >0500,>1300,0,>0500,>0008
TEXT ' '
PBD TEXT '.PBD'
*
* LOAD PAB BLOCK FROM SOURCE DRIVE
MOV @CRULST,R12
MOV @SAVADD,R9
LI R8,PBDPAB+9
SBO 0
AI R9,4
MOVB *R9,R4
MOVB *R9+,*R8+
SRL R4,8
GETORG MOVB *R9+,*R8+
DEC R4
JNE GETORG
SBZ 0
MOVB @PBD,*R8+
MOVB @PBD+1,*R8+
MOVB @PBD+2,*R8+
MOVB @PBD+3,*R8
AB @B4,@PBDPAB+9
LI R0,>12A0
LI R1,PBDPAB
LI R2,50
BLWP @VMBW
AI R0,9
MOV R0,@DSRADR
BLWP @DSRLNK
Very elegant!
I also set up the number of potential open files by calling the disk controller subroutine to limit it to one. This particular technique is a little different than the one presented by Bruce Harrison in his Art of Assembly articles. He uses the version called by BASIC, which is a little more cumbersome than just calling the subroutine directly.
Here's what that looks like:
B1 BYTE 1
*
* SET NUMBER OF ACTIVE FILES TO ONE
LI R0,PAB
MOV R0,@DSRADR
LI R1,>0116
BLWP @VSWW
MOVB @B1,@>834C
BLWP @SUBLNK
One important note here is that you must not call this BEFORE accessing the last drive for the first file above. Calling subprograms uses the same technique as file manipulations, and it wipes out the entry in the controller card.
You may be wondering "What's SUBLNK?" Well, that's a short modification I made to the DSRLNK routine. I thought that having the linkage type stored in the DATA word after the DSRLNK call was a bit wasteful. So instead, I encoded it into the DSRLNK itself; SUBLNK just feeds the value 10 instead of 8 into DSRLNK. You can easily replace the above with:
BLWP @DSRLNK
DATA 10
You may also be asking "What's VSWW?" Well, since I wrote my own video utilities, I added a few extra:
- VSWW - VDP Single Word Write
- VSWR - VDP Single Word Read
- VMBS - VDP Multiple-Byte Stream
This lets me fetch a whole word out of VDP or write one there without having to involve R2, like I would if I used VMBW. VMBS lets me write a single character in the high byte of R1 continuously for R2 bytes. You can find the code for these in the UTIL_S file.
You may also wonder why I'm using BLWP for my video utilities, since common knowledge has always indicated that using BL was better. Well, my reasoning there was that for the most part, all my video operations are not timing-critical. And it's useful to have R0-R2 in the parent routine preserved rather than destroyed. If there's a section where speed is essential for video, you'd best not use ANY branching at all, but rely on the direct ports and code the work in directly.
An interesting point with the INIT_S routine is that since nothing here is ever re-visted in the program, the codespace that it occupies can be, in fact, reclaimed as buffer space later. Caution needs to be taken, though, that you don't overwrite code you WILL need. The source listings that the assembler can generate as an option is useful for seeing how large a block of code can be. In this case, there's nearly 300 reclaimable bytes here, not too shabby at all.
The LOAD_S is the next step in the program. This file takes care of loading files that are changed more frequently. The program jumps back here anytime it needs to do a reload of a map or anything new.
Most of the data loading is fairly straight-forward, but there's some special cases that can come up. One is the potential to change the colors in the graphics sets. I had some cases come up with maps where I really wanted some different colors, but it was wasteful to add a whole new identical set in a different color. So I used some of the map header space to store "color change" instructions. This section is as follows:
LDG1 LI R0,>1228
LI R1,STRING
LI R2,8
BLWP @VMBR
MOV *R1,*R1
JEQ LDMG
MOV R1,R3
LI R4,4
LDG2 MOV *R3+,@WORK
JEQ LDMG
MOV @WORK,R1
ANDI R1,>00F0
ORI R1,>0001
SWPB R1
MOV @WORK,R2
ANDI R2,>000F
SLA R0,3
LI R0,>2000
CLR R5
MOVB @WORK,R5
SRL R5,5
A R5,R0
BLWP @VMBS
DEC R4
JNE LDG2
* Next section...
LDMG
Up to four color changes can be done, with up to 16 consecutive characters alterable. If it encounters a zero in any of the color change words, it immediately stops and moves on with operations.
The final file to look at is where a lot of my disk operations occur, the FILE_S file. This contains disk operation commands specific to the program.
The primary one is BLDPAB, a subprogram which constructs the PAB block for a file from the PDB data block in VDP, and also calls the DSRLNK to either open the file, or load/save the memory-image. It also contains a call to DSKMSG, a subprogram that, at present, does not exist. This subprogram will prompt the user to insert the proper disk, if a swap was indicated in configuration. Since the demo is running off a single disk, this utility is at present not needed.
There's a number of times in the program that only a single record needs to be loaded from a file. So I wrote the LD1REC program to handle this. It opens the file, reads the record, and closes it. For situations where multiple records need to be loaded, the code to handle this is handled directly in the parent routine. It's not good to be calling subprograms for disk operations if you can avoid them; it adds to the overhead.
The DSRLNK routine used in my program was originally written by Travis Watford, and referenced in the Art of Assembly articles. I copied the code and was delighted to find that it worked perfectly. (And is a heck of a lot more readable than the original TI ROM DSRLNK.)
I made a few adjustments to it. One was so that it did not look for a DATA word following the DSRLNK call, and that a subprogram could be called through it through SUBLNK instead. The other was to preserve the PAB address stored at address @>8356 in R7 for the bulk of the DSRLNK operations. This lets me call DSRLNK multiple times for one file without having to restore the address every time.
Here's the complete listing:
GPLWS EQU >83E0
VWS EQU >8320
VDPWA EQU >8C02
VDPRD EQU >8800
VDPWD EQU >8C00
DSRADR EQU >8356
NLEN EQU >8354
CRULST EQU >83D0
SAVADD EQU >83D2
DSRWS5 EQU VWS+10
DSRWS0 EQU VWS+1
DSRWRK BSS 64
DSRLNK DATA VWS,DSR
SUBLNK DATA VWS,SUB
SPACE DATA >2020
B46 BYTE >2E
B170 BYTE >AA
*
SUB LI R5,10
JMP DSR0
DSR LI R5,8
DSR0 SZCB @SPACE,R15
MOV @DSRADR,R0
MOV R0,R7
MOVB @DSRWS0,@VDPWA
NOP
MOVB R0,@VDPWA
AI R0,-8
MOVB @VDPRD,R1
MOVB R1,R3
JEQ DSR9
SRL R3,8
SETO R4
LI R2,DSRWRK
DSR1 INC R4
CI R4,7
JH DSR9
C R4,R3
JEQ DSR2
MOVB @VDPRD,R1
MOVB R1,*R2+
CB R1,@B46
JNE DSR1
DSR2 CLR @CRULST
MOV R4,@NLEN
INC R4
A R4,@DSRADR
LWPI GPLWS
CLR R1
DSR2A LI R12,>0F00
DSR3 SBZ 0
DSR3A AI R12,>0100
CLR @CRULST
CI R12,>2000
JEQ DSR8
CRUOK MOV R12,@CRULST
SBO 0
LI R2,>4000
CB *R2,@B170
JNE DSR3
A @DSRWS5,R2
JMP DSR5
DSR4 MOV @SAVADD,R2
SBO 0
DSR5 MOV *R2,R2
JEQ DSR3
MOV R2,@SAVADD
INCT R2
MOV *R2+,R9
MOVB @NLEN+1,R5
JEQ DSR7
CB R5,*R2+
JNE DSR4
SRL R5,8
LI R6,DSRWRK
DSR6 CB *R6+,*R2+
JNE DSR4
DEC R5
JNE DSR6
DSR7 INC R1
BL *R9
JMP DSR4
SBZ 0
LWPI VWS
MOV R7,@DSRADR
MOVB @DSRWS0,@VDPWA
NOP
MOVB R0,@VDPWA
NOP
MOVB @VDPRD,R1
SRL R1,13
JNE DSR10
RTWP
DSR8 LWPI VWS
MOV R7,@DSRADR
DSR9 CLR R1
DSR10 SWPB R1
MOVB R1,*R13
SOCB @SPACE,R15
RTWP
Feel free to use this, or any other utilities in your programs. Although it's always nice to get a line of credit in that case.
Conclusion
I'm probably not going to be updating monthly from now on. Instead, I will publish a new article when I have achieved a new milestone with the demo worth looking at. Until then!