Dragontorc disassembled
Dragontorc
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 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
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");
}
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.
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.
'I think everyone who has not coded should go to www.breakintoprogram.co.uk and follow the instructions as...'
ReplyDeleteHyperlink is invalid file:///C:/Users/steve/OneDrive/Documents/www.breakintopogram.co.uk