Dragontorc disassembled




Dragontorc


Dragontorc was perhaps my favourite of all the games I developed over the years. It was written in the days before I started working with an assembler program. for the uninitiated an assembler allows you to write in human readable code and it turns it into machine code that looks like goobly de gook but the computer can execute.

I used to convert my hand written source code into hex by hand using a couple of look up tables that I made copying from a Z80 book  and  put them in a small crib book that I kept on my desk next to my HEX calculator. I used to input the resultant machine code into Spectrum BASIC and run a program that read the REM statements and poked the machine code into the Spectrum RAM.

So the only source code that existed was my handwritten source and disassembly listings that at least included my 4 character labels. As I debugged it and changed it the sheets got very messy. I recorded the changes and the module version that included the change. The sheets were almost unreadable at the end I rarely rewrote them. I later used to print out a disassembly on fan fold paper and write corrections on that.  I hacked into the disasssembler code  to enable it to read my  Hex Loader label table.  Unfortunately over the years most of the  listing and the original paper sheets were thrown away. I have a listing of the general subroutines marked with changes for Astroclone.

 

The only surviving copy of some of the Dragontorc source code.

 I thought I would test the possibility of recreating the source code from an image of the production game. Then I could annotate it and publish it , rather than it all just be in my head and a folder of scrappy notes.

I researched the process on the internet and the process was as follows:

Stage 1. Download an image file of Dragontorc from an archive site. I used World of Spectrum to source the image file. I had a false start where I used a Spectrum emulator and saved my own image. The problem was I had started the game so some initial data was overwritten. At the time I was just testing the feasibility but carried away and started annotating the code. This gave my issues later with the data on so I then downloaded a new virgin image  file .

Stage 2. Decompress the image file.

The image file is in a compressed form rather than a byte for byte image of RAM.  I sourced some code from the internet and corrected it to unpack the image file into a full dump of the RAM from 4000H  to FFFFH which is the Spectrum RAM for the 48K machine. Here is the code I used and adapted to use my C core routines to read and write files and allocate memory. I sourced it  from the internet, I cannot remember where I found it, apologies to the original author for the lack of a credit.

// Decompress the data in the Spectrum image  file.

/// </summary>

void Decompress()

{

       UWORD Page;

       int HeaderLength = 30; //always present

       char* pBuffer = NULL;

       char* pOut = NULL;

       char* pOutBuffer = NULL;

       char* pIn;

       int Size;

       int i;

       int j;

       int Count = 0;

       UWORD* pExtend;

       UWORD Repeat;

       UWORD Extra;

       char Value;

       int BlockLength;

       int Processed = 0;

//allocate a buffer big enough for 64k just in case

        MemoryAlloc(&pOutBuffer, 0x10000, MEM_NORMAL_CLEAR, "out");

       pOut = pOutBuffer;

       FileLoad(&pBuffer, FILE_Z80); //allocate a buffer and read the image file 

       Size = FileSize(FILE_Z80);  //get the file size   

       pExtend = (UWORD*)(pBuffer + 6);

       pIn = pBuffer + HeaderLength; //bypass first header

       Processed += HeaderLength;

       if (*pExtend == 0)

       { //is version 2 + byte 30,31 contain the additional  header length  v2 23  v3 54 or 55

              Extra = *(UWORD*)(pIn);

              Extra += 2;  //add on count size

              pIn += Extra;

              Processed += Extra;

       }

       while ( Processed < Size)

       { //loop thru blocks

              if (*pExtend == 0)

              { //is blocked

                     BlockLength = *(UWORD*)pIn;

                     Processed += 3;

                     pIn += 2;

                     Page = *pIn; //expec page 8 4000-7ffff    page 4   8000-BFFF    page 5 C000-FFFF

                     pIn++;

              }

              else

              { //v1

                     BlockLength = Size - ( HeaderLength + 4) ; //end block is 00EDED00  not tested for assumed last in file

                     Page = 0;

              } 

              if (*pExtend != 0 || BlockLength != 0xFFFF)

              { //compressed)

                     for (i = 0; i < BlockLength;)

                     {

                           if ((i < BlockLength - 4) && *(UWORD*)pIn == 0xEDED)

                           { //compression seq

                                  pIn += 2;

                                  Repeat = (UWORD)((UBYTE)*pIn);

                                  if (Repeat == 0)

                                  { //end mark

                                         break;

                                  }

                                  pIn++;

                                  Value = *pIn;

                                  pIn++;

                                  i += 4;

                                  for (j = 0; j < Repeat; j++)

                                  {//could be 1 to preserve genuine EDED

                                         *pOut = Value;

                                         pOut++;

                                  }

                                  Count += Repeat;

                           }

                           else

                           { //just copy

                                  *pOut = *pIn;

                                  pIn++;

                                  pOut++;

                                  Count++;

                                  i++;

                           }

                     }

              }

              else

              { //uncompressed block

                     memcpy(pOut, pIn, 16384);

                     i = 16384;

                     pIn += 16384;

                     pOut += 16384;

              }

              Processed += i;

       }

       //check that it worked

       LOG_FATAL(Count != (int)pOut - (int)pOutBuffer, "count wrong");

       LOG_FATAL(Size != (int)pIn - (int)pBuffer, "in processed wrong");

       LOG_FATAL(Size != Processed, "in counts wrong");

       FileSave(FILE_BIN,pOutBuffer,Count); //save the .bin file from the buffer

}

Stage 3. Disassemble the unpacked file.

I tried a few disassemblers but in the end used DASM at it seemed to be the easiest to set up so I could keep running it from a little cmd file  which just had the line

Dasmx.exe   -a -c Z80 -e 0xBAD1 -o 0x4000  dragontorc.bin

This tells the disassembler to create an assembly listing using CPU type Z80 and with program start point at 0xBAD1 (Hex). The  -o parameter of 0x4000  tells the disassembler to add 0x4000 to all the locations as the RAM file  started at 0x4000. Otherwise the assembly produced would start at 0 so all the addresses would be wrong.

The DASM disassembler is quite clever and will try to work out which bits are code and which bits are data.  If t tries to interpret data as code you just get an unrecognisable mess of a listing. I could not get this to work very well so used a parameter file to tell DASM where to expect code and where to expect data. Luckily I had some old notes on the memory map but it was not clear as had changed in the last few updates. It gave me enough of a clue to try it out see what is produced and then to refine the hints until I got a reasonable listing. For some reason there were occasional bits of code it interpreted as data or vice versa. A few bits I corrected by hand.

The .sym file I used to tell DASM where to find code and data. The first number is the start address of each block in hex, the second is the length. The name in the middle is the label to put in the source code listing.

skip          0x4000 0x21A9 ;screen

byte          0x61a9 Tables 0x1Bfe

code          0x7da7 M04 0x00c41

byte          0x88F8 RoomGS 0x0EE1

byte          0x97D9 Graphs 0x22f8

code          0xBAD1 M02 0x1D33

code          0xD804 M03 0x124A

byte          0xEA4E  Vars 0x237

byte          0xEC85 Text 0x032c

code          0xEFB1  M01 0x026f

byte          0xF220  Data 0x0DE0

Stage 4.  Set up a debugging environment to test the assembly code is complete and correct

I tried to use a Visual Studio extension at first  called SpecNetIde. Its description was great, it could assemble and the debug uisng an emulator that was all integrated into VS. I just could not get it to work. The assembler was fine, I used that at first to check out the disassembly and even got it running on the emulator a couple of times. I tried hard but could not get the debug session to work. More often than not when I loaded the project into VS it failed to load the extension properly so I had most of the commands I needed greyed out. The only solution was to close VS and start again, and again until the new commands were there. Then I found communication with the emulator was extremely dodgy. I tried for several weeks but could not get debugging to work.  One of the issues I had was that VS kept crashing whenever I tried to  (or accidently) moved a window. That actually turned out to be a Microsoft issue caused by an OS update but it added to the overall frustration. Then to cap it all my laptop would not start my user profile one day and when I looked the whole lot was gone along with all the things I had not backed up. The source code I had backed up but all the downloads of the bits and pieces and the disassembly parameter files were lost. I never found out what caused windows to delete my entire User  data. You may back up things like documents but you cant even back up AppData as some of the files are locked in use. That means if this happens you painfully have a reinstall  job of all your applications and lose loads of things each App saves behind the scenes. I tried with an undelete tool but the latest Zone alarm virus update had already stamped all over my lost files. It reminded me of the days where I tapped the Spectrum keyboard too hard , the RAM wobbled and I lost all my work. I always kept about 8 generations of backup for each module. That meant a box of about 64 tapes constantly being cycled.

So months later when the pain had subsided I thought I would have another go. I came across a website www.breakintoprogram.co.uk describing a development stack and thought I would give it a go. It used Visual Code which I consider to be a clumsy cut down version of VS but at least it works. It interfaces with a choice of Z80 assemblers, I followed the breakintoprogram methodology using Sjasmplus.  You run a separate Spectrum emulator and Visual code communicates with it. This I found a bit dodgy. Now and then I have to use task manager to close down an unresponsive and uncommunicative emulator. However as a system it is quick and workable and beats the early ways I used to write on the Spectrum.

I tried a run of my assembled Dragontorc and it gave a recognisable display of the control selection screen. Some text was corrupt and it crashed after this screen. I look at the text in the source and it was also corrupt. I tinkered with the disassembly and regenerated some parts of the data and hey presto I got the first screen up and running. The next issue was the attribute colours that were loaded as a screen dump were corrupt.  I found were I had been cutting and pasting various disassemblys together I had part of the RAM image as code and as data that overlapped.  Again a bit of tinkering getting the sizes correct and I got the game running. Some of the objects were missing from the first room  so I had a closer look at the data.

The first disassembly running, but where is the fire and Maroc's true self.


Stage 5.               Adding meaningful labels.

A disassembly makes up labels for each jump or item of addressed data that is usually the HEX address with a L in front of it. The code is pretty unreadable like that so now the challenge is to decipher what each routine or variable is and give it a name.  If you can find and name the work subroutines it gives you clues as to what the rest of the code does. Luckily I had an old listing to give me a head start. I used to use 4 letter labels such as WMVX  meaning MovementX. I used to remember what these were 30 years ago but now can only guess most of them. I did have a few surviving notes that  described many of then. First I replaced made up labels with my old labels. Now I am gradually replacing those with names that are self explanatory. For example the start of the program begins with

M02:

       ld     hl,XEC7D

       inc    hl

       ld     (XEA58),hl

       ld     hl,07EF4H

       ld     (XEB57),hl

       ld     hl,061ACH

       ld     de,XEB38

       ld     bc,0001AH

       ldir

       ld     hl,Graphs

       ld     (XEB55),hl

       ld     (XEAC8),hl

       ld     hl,RoomGS

       ld     (XEB36),hl

       call   M01

I looked at the first two lines , why would I load HL with the wrong number and then immediately increment it, when I was fighting for every byte of room.  Anyway here is the labelled version of the same bit:

M02:

       ld     hl,PlotObjects-1

       inc    hl     ;?

       ld     (pPlotObjects),hl ;init ptr to plot objects

       ld     hl,TextTable

       ld     (pTextTable),hl ;init ptr to Text table

       ld     hl,TableAddresses ;copy 13 data table addresses to this module

       ld     de,pRoomTable

       ld     bc,0001AH ;13 *2 bytes is longer than the no of ptrs

       ldir

       ld     hl,Graphs

       ld     (pGraphics),hl ;graphics address 

A lot more readable but does not explain the anomaly at the beginning. Perhaps I just did not notice on my hand written source with all its rubbings out and corrections.

It has been great fun revisiting the game. When I first finished the game I thought that it would be good to go back and play it when I had forgotten all the details of the adventure. When you are debugging a game its not quite the same as playing it as a player. You get so far, it goes wrong , you fix it and repeat. So its all in stops and starts. I remember it was still a fun time to play test it. We used a crib sheet to get through the adventure as quickly as possible. It took a few hours to complete  the whole thing. The save game was invaluable when testing the adventure sequence. When I got to the end stage and tried to destroy the crowns it went wrong. I had put the wrong event code on one of the crowns. At least I could fix the data and load my last save game to check the correction.  Andrew and I both plated the final version all the way through to double check it.

I think there was an issue whereby you could accidently kill a living hand that was essential for the adventure. A fixed version was sent to update the master but at least one copy on the web has the original issue. There is a tip saying "dont kill the hand". Checking that things did not happen was the most difficult part of testing. Think of how many combinations of objects there are. Most of this had to be done by desk checking the event data to make sure events  on important items were unique and only matched the corresponding triggers.

Deepest Blue

Its been a while since I blogged about this.  I have been pushing to get it finished but its not lying down yet. The bulk of the work has been systematically testing all the mission types and correcting any issues, then starting again to make sure they all still work.

I redesigned the ships to get rid of a few niggles. They now have their tow points at the back so container trains do not crash into the hull when the ship turns.  I added two bigger sets of ship parts so I can build a more varied range of sizes. There are also joining pieces so you can mix and match hull parts. I added medium and large turrets, weapons, engines etc. Adding new models showed bugs in the model generation system.  Each model part is made of an extruded cross section. For joiner pieces I needed different cross sections at each end of a hull piece. The model builder has to create dummy vertices so they can be matched and add them at the correct places. at first the algorithm was not symmetric and it made it look as though the hull had been twisted by a giant pair of hands.

Constructor class ship. Note profile display on left


I also added gun towers to the bases so they could defend themselves. These use the same code as the ship turrets but did not work at first due to not being set up when the base was set up.  I tested them and tested the code for being a criminal at the same time by shooting up my own bases until they got fed up, criminalised me and shot back.

I had fun testing the new ships. You can just try out a ship on a test scenario where a Seiddab fleet is waiting for you.  The gun turrets are pretty deadly when they lock on. It allows you to fly past an enemy while they rake it with fire. They also protect your back while you concentrate on enemies in front. You can opt to control front, left, right or rear banks manually and this gives you the corresponding view.  It is pretty weird changing direction when you are not looking forward. The big ships with turrets are not that manoueverable so that is not too much of an issue, and the guns steer themselves onto the target. I do find myself trying to help them by adding a bit of steering.  The idea is you select targets for the bank matching the direction you are looking, the AI does the rest. There is an override so you can get all the guns to try to track to your selected target but most of the time its best to let the other guns do there own thing. There is still a few bugs to be ironed out. I took out a destroyer for a test run and the guns were reluctant to fire at any designated target. You see a HUD gun bank track and lock on which shows the guidance system for each bank has tracked and found the target, the guns seem unwilling to follow and seem to have there own targets. Gun turrets can belong to more than one gun bank, I need to check that a player request has priority over an AI request when two gun banks sharing a turret have different targets. I ask myself how it got that complicated, but as soon as you want multiple turrets and you realise the player cant control all of them you need the AI to assist (or take over completely but that makes the player redundant).

Broadside view


I realised I had a potential issue with remote games as I had an identity system whereby each ship etc has a unique id. If a ship dies these get recycled. The issue is that structures such as missions may refer to dead objects and if the Id is reused the mission would refer to the wrong object. I revised the Id system so deal with this so they have a unique key.

Some of the issues I get now only happen after a while. I have loads of checks to make sure everything is running fine. If I get a controlled  break at one of these tests and cannot figure out how it got to be in that state I add more tests and let the program run on. As there are hundreds of ships running all the AI programs to a large extant the game can test itself. I wander about in one of my ships, after sending all the others do do various missions I want to check. I then flick from ship to ship checking everything is going fine. If I notice another ship doing something odd I target it. I can then break the program during that ships to see what it is doing. Today I noticed ships that just sat going nowhere. When I looked they had no thrust. I traced the update and found they had no engines. Sure enough I had forgot to add engines to this class of ship.

I added a few features that I had planned but had not coded:

Rack features  that can be added to a ship to boost performance or jam enemy targetting.

Alien artifacts that can be collected to upgrade the technology.

Anti missile defenses. 

Incoming missiles

Missiles

 I had coded most of the missile stuff years ago but had to add the Ai decisions to use missiles. I added a missile count so missiles were limited but deadly. I added anti missile decoys and the ai code to deploy them. I then added a rack feature to jam enemy missiles and a corresponding anti jam rack. I added missile trails so you can see them homing  on you.


 I had to redesign the HUD cons to get room to add the rack buttons. I used to have a button for each HUD type but as they are action specific depending on the target you only need the option to open the target specific HUD. For example the default is the navigation HUD. If you select a Warp exit the HUD gives you the option to show the Warp HUD which also allows you to warp. If you select a friendly base it gives the the option to open the Docking HUD which also request landing permission and allows you to dock with the base.

So I am drawing a line not to add any more functionality, just to get all existing stuff working.  I shall be adding the special effects and revising the graphics as the coding phase draws to a close.  I must not get too distracted by Dragontorc. Sometimes I have Deepest Blue running while I am doing a bit of Dragontorc. Its been running while I type this blog but has just stopped at a test I put in to find out why a ship aborts docking with a base. There were about 16 stages of the docking process this could happen , usually because it or the base has been destroyed. It turned out to be a new bit of code that gets rid of the target if it enemy and out of radar range. That is so often the case that it is the latest changes that cause a problem.  I often use GIT to show all the latest changes to check them. It would be better with a four eyes check but I only have two.

Close encounter with enemy cruiser


 Programming Talk


I think everyone who has not coded should go to www.breakintoprogram.co.uk and follow the instructions as to how to set up a Spectrum development system. Then you can start to program little assembler programs that do things with the screen. One of the easiest things to play with at the start is the character sized colour map, the attributes. If you write data to this it is safe as long as you do not go over the end, and you can see what you have done as you step through your program.  For example You can colour the screen black by moving zero to the attribute bytes. They occur at address 5800H   and are 300H long. If you are unsure of hex play about with the Microsoft calculator in programmer mode. 

Z80 Is Easy

Forget any preconceptions, Z80 is easy if you take it step by step. You can start off by knowing just a few instructions and then add to them. Remember that they work right to left. So here is a primer for the complete beginner. There are plenty of good references online. I am only trying to convince you to try.

Registers

Assembler instructions refer to one or more registers. These are little stores of data on the Z80 chip that it came  manipulate very quickly. Single registers are 8 bits in size. That means they can hold a number from 0 to FF in hex, (which is the same as 0-256 in decimal). I advise you to get to know hex. Its easy once you get the hang of it you just count in 16,s rather than tens. 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F. That is just five extra digits to remember. A = 10, B=11, C=12, D=13, E=14, F=15.   Why do I prefer hex? Its because everything on early computers is done in powers of two.  The Spectrum Screen is 32 character wide. Rather than learn a 32 times table its easier to think of multiples of 20h.  So the first colour attribute row is at

5800h. The next is at 5820h. The third is at 5840h  etc .

If we just want to go along the screen we merely add 1 to the attribute address,

5800H,58001H,58002H etc

So lets write our first program

    ORG C000H  ;this tells the assembler where we want to put the code in RAM

    ld            a,0          ;loads the A register with 0 for black

    ld            hl,5800h               ;loads the HL register pair with a 16 bit number of the attributes

    ld            (hl),a                     ;loads the value from the a register into the location specified by HL

    ret                                        ;return to caller

And there we have it a black square in the corner of the screen.  If I would put a breakpoint at the end so it did not return into oblivion.  It would depend on the dev system what actually happened. Return means take the address last put on the stack and set the CPU instruction pointer to run code at that location.

 The action of each step reads from right to left for some reason. Think of the comma as being like an equal sign, for example for ld a,0 read  load a=0.

So lets draw a line along the screen. We could do this trivially by increasing the HL register and repeating the ld (hl),a

adding

inc hl      ;step on to next attribute

ld (hl),a

inc hl      ;step on to next attribute

ld (hl),a

...etc

After a while this gets boring to type. Its quicker to set up a loop. Z80 has a brilliant instruction called djnz meaning dont jump not zero. It decrements the b register and then tests the result. If the result is not zero it  jumps to the specified label.

So we can write a simple loop

                ld            b,32       ;count of 32 characters wide

                ld            a,0          ;loads the A register with 0 for black

                ld            hl,5800h               ;loads the HL register pair with attributes address

LRow:    ld            (hl),a                     ;loads the value from the a register into the location

                inc          hl           

                djnz       LRow

                ret ;return

What if we want to colour in a square block. We just add another loop. We also need to add to the hl register pair after the first loop to step on to the right position on the next attribute row.

                ld            c,16

                ld            a,0          ;loads the A register with 0 for black

                ld            hl,5800h               ;loads the HL register pair with attributes address

                ld            de,16                     ;prepare de with the screen width - the  block width

LRowStart:

                ld            b,16       ;count of 32 characters wide

LRow:   

                ld            (hl),a                     ;loads the value from the a register into the location

                inc          hl           

                djnz       LRow

                add        hl,de

                dec         c              ;decrement our row counter his will set a zero flag when it is 0

                jr             nz,LRowStart ;   ;jump relative when flag is not zero

                ret

It is that simple. Work in steps and keep making it just a little more complicated. Try drawing a number of squares in different colours. Try erasing a square by putting back the original colour. If you can draw and erase a square you can draw it in another place an hence make it move. This is the basis of animation. Or start with a small square in the centre and make it grow. Then add another square of a different colour and make that grow. Keep repeating that and you have the start sequence to 3d Space Wars.   

Storage

Sooner or later you run out of registers. Then you can store data in ram. he easiest way to do this is to use the a register.

                ld            (StoreCount),a  ; loads the a register into the byte location at StoreCount              

                ld           a,(Storecount)   ;loads the value at StoreCount into the a register

We can define the byte store with :

StoreCount:      ;this is a meaningful name we use to refer to this data

                db           0              ; db means 1 byte with the iniital value of 0

we call call any of our routines by Starting then with a label and calling it

DrawThings:      

                call DrawSquare                ;call our subroutine

                call DrawRectangle          ;call a similar routine

                ret                          ;makes this its own routine that can be called

DrawSquare:

                ;routine here

                ret

DrawRectangle:

;another routine here

                ret

You can build up huge nests of routines calling other routines.  You can pass data to subroutines in registers or by storing in RAM as shown above and the subroutine referring to the same store. This can quickly become complicated remembering which routine does what to a byte of data. Experience has taught me to limit the combination of routines and bytes. It is better for one function to "own" a data store and be responsible for updating it. Others can read it but it is poor practice for many routines to update data will nilly.

It is easy if you take it step by step. The complications come when you try to scale it up to doing something complicated without breaking the task into simple steps.  It is so absorbing, I have spent most of my life coding. Many years ago I wrote a series of articles entitled "So You Want To Be A Programmer". Some of those readers now own their own software firms.  I hope I can inspire even more people to have a go and start coding. The Spectrum is such a lovely little machine to experiment with and a safe environment.

 

 

 

 

 

 

Comments

  1. 'I think everyone who has not coded should go to www.breakintoprogram.co.uk and follow the instructions as...'
    Hyperlink is invalid file:///C:/Users/steve/OneDrive/Documents/www.breakintopogram.co.uk

    ReplyDelete

Post a Comment

Popular Posts