For
kicks, it seemed fair to do the same thing in Thunder Force III.
However,
Thunder Force III doesn't /have/ a lives select, only a 'ship speed' and 'game
level' option. This makes things a bit harder!
I started by going back to
the hacking documentation I linked in an earlier blog, and looking for anything
to do with player lives. The function list included one called 'StyxCollision'
at $3f56 that seemed promising, so, I set a breakpoint there. Then, I started a
game, intending to die.
Unfortunately, this was called immediately, and
didn't seem to have much to do with lives. The next one, "StyxFailure" at $4178,
seemed to work and was called when I collided with an enemy. Okay
then!
At first glance it didn't seem to do much, so I stepped through.
When it eventually did a bsr to $532c, I checked the notes and saw that was
PlaySound1. So that made sense. I skipped over the call and looked a little
further, and then at $41AE hit this:
subq.w #1,$fff2b4.l
bmi $41c2
$F2B4
contains '4', which is reasonable for this. So, to test I found the right
address, I set it to $63, then continued. This worked! (And when the collision
code was immediately called again, I found that $A808 contains the countdown for
how far forward the new ship moves, and while I'm not sure what $A802 is meant
for, setting it makes you invincible.)
So next was the issue of how to
select 99 lives on the options menu. Mostly I found myself eyeing ship speed,
since it seemed the least useful (you can change speed in game, don't need it
here!) I also had to decide just how hacky to be... in the end, I decided to go
ahead and just replace it altogether with a plainly labelled 'lives'
count.
You may remember my previous TF3 hack for weapon sounds reserved 4
bytes at the top of RAM, and I went through a lot of work to protect those bytes
(probably foolishly), then only used 1 bit. Well, now we can use another byte.
My first hack pokes around with $FFFE.w, so I'll go ahead and use $FFFC.w as the
values for setting lives. Someday, I may wish I used bytes, but hopefully this
will be the end of it.
That decided, there are three things to do: 1)
update the menu for numeric entry, 2) store the result at $FFFC, and 3) use that
value when starting the game.
For the first thing, it seemed easiest to
replace the text first off. To save needing to hack the checksum every boot, I
turned autofix checksum on. I try to leave that off because forgetting then
burning an EPROM really sucks. ;)
But this will be enough trouble as it is.
First, I had to find 'SHIP
SPEED'. That's easy enough with most hex editors. There was a catch, of course,
the space was not ' ' but rather '@' (searching just for 'SPEED' found it). It's
always important to repeat your search to make sure there is only one hit, to
make sure you're in the right place - in this case, there was only one
hit.
Rather than worry about how it determines string length, we can just
pad it with spaces.
So, at $6854, replace "SHIP@SPEED" with "LIVES@@@@@".
A quick test showed that worked fine and did not appear to interfere with the
game.
The next part is tricky. The display for that line displays the
text "LOW/MID/HIGH/TOP", and we want numbers. I doubt I'd even have attempted
this without the hacking document, since they did all the work already. (Of
course, we could also have written completely new code, but this was supposed to
be quick, like TF2 was!)
The memory map in the hacking document shows the
velocity select in the options menu is located at $69B2, with the texts at
$69D8-$6AEA. To start, we'll take a look at how that
works.
Interestingly, that function is called twice on entry to the
Configuration Screen, but it is also called when the entry is selected. We see
this code. I poked around a bit to verify the intent of the code:
move.w #$14,$f20c.w value is 20 (mid screen, maybe?)
move.w #$27,$f20e.w value is 39 (?)
move.w $f34c.w,d0 get the current value of speed setting
and.w #$3,d0 mask down to 0-3
mulu.w #$5,d0 multiply by 5, each word is padded to be 5 chars long
movea.l #$69d8,a0 $69d8 is the ROM location for the word 'LOW '
lea (a0,d0.w),a0 adds them together to get a final address into A0
bra $4bfe almost certainly a string display (doc calls it 'DisplayTxt')
So
that is where the string is displayed. It's not really what we wanted, but we
know now that the address being changed is $f34c, so we can set a breakpoint on
that memory location instead. For this, moving left made it breakpoint at $6980,
at a BCC. I disassembled from a little earlier and found this at
$6976:
btst #$2,D0 check for joystick left
beq chkright not set, jump to $698a
subq.w #1,$f34c.w speed setting down one
bcc notneg jump if not negative
clr.w $f34c.w reset a negative value to zero
notneg:
bsr $69b2 display velocity string (we just looked at this above)
bra $6960 presumably, loop around and repeat the scan loop
chkright:
btst #$3,d0 check for joystick right
beq notright branch if not set to $69a6
addq.w #1,$f34c.w speed setting up one
cmpi #$3,$f34c.w check for maximum of 3 (0-3)
bls nothigh branch if not too high
move.w #3,$f34c.w reload value with 3
nothigh:
bsr $69b2 display velocity string
bra $6960 loop around (presumably)
notright:
bra $6960 loop around (odd that it comes here for nothing, but not unusual)
So
that definitely covers the menu entry, and it's what we'll need to change.
Presumably the changes are already starting to look obvious, but there's the one
problem of not knowing how to display digits instead of a string. For that,
let's take a peek at the music select line.
The document has something in
the area for music test at $6880, so we'll start with that. Alas, breakpointing
there does nothing (it appears that is just the text). We also see
'MusicTestData1' at $6b18, so I tried a breakpoint on reading there and select
the first music. That worked, and stopped at $6AF0. So I disassembled back a
bit, and found this at $6AAA:
btst #$2,D0 check for joystick left
beq notleft branch if not set to $6ac2
subq.w #1,$f344.w decrement music index
bcc nowrap branch if it didn't go negative
move.w #$15,$f344.w it went negative, so wrap around to $15 (21)
nowrap:
bsr $6b4c draw routine? hacking document does NOT cover this.
bra $6a8e presumably loop around
notleft:
btst #$3,D0 check for joystick right
beq notright branch if not set to $6adc
addq.w #1,$f344.w increment music index
cmpi.w #$15,$f344.w check for maximum
bls nowrap2 branch if not too high
clr.w $f344.w set to zero on wrap
nowrap2:
bsr $6b4c draw routine?
bra $6a8e loop around?
notright:
and.w #$f0,d0 mask off just the buttons
beq $6a8e loop around if none pressed
movea.l #$6b18,a0 get address of music table...
From
there it just handles looking up and playing the music. But that's not enough
information, we need to look into $6b4C to see how the digits are drawn (and
importantly, positioned!) Breakpoint there and change the
number...
move.w #$14,$f20c.w loads '20', probably x position
move.w #$2b,$f20e.w loads '43' - something to do with Y.. 4 more than the speed row which is right...
move.w $f344.w,D0 loads the value of the music index into D0
bra $4dce branch to $4dce - also not in the hack doc, but it seems likely we got it.
Note
how this ends with a BRA, but is called with a BSR. This means the RET in the
$4DCE subroutine will return to our caller.
The 'y' coordinate is weird,
but, it is predictable based on what we already know. So let's just go ahead and
see what happens if we use it.
Right about this time I realized that it
was going to be a bit of a pain making it FIT, because the speed select code
does not wrap around at zero, and uses a bcc rather than comparison for the
lower value. These are smaller, faster operations, but they mean that it will be
hard to fit the replacement code in the same place. Also, it bugged me a lot to
be replacing the speed select.
I decided instead, to replace the voice
test. It was already numeric, and it wraps. It wraps at zero, but because it has
a button press as well, there's a little more space for the replacement code.
And we don't lose the (arguably not very useful) speed setting.
Since the
only hack so far was the speed text, I went back and undid that. But now, of
course, I had to find the voice test line.
The text was easy, found that
at $68AC and changed "VOICE@TEST" to "LIVES@@@@@". For the test itself, the
document shows the VoicesPtrTable at $73478, so that was the address I watched.
That didn't work though.
At this point, with not much to go on, I
disassembled the code after the above, and just looked for more joystick reads.
I found a set at $6B82, modifying $F346, which was likely sound effects. Another
one at $6c16 proved to be the one, however. Very similar to the music test
above, except with a range of 0-6, and updating $f348.
With that choice
made, and only mild misgivings, I went ahead. Since more than ranges had to be
changed, it was easier to use the assembler and just overwrite the whole block.
Its loop point appeared to be $6BFC, otherwise I was free to replace code. We
also have to remember that the life counter ($f2B4) in this game contains the
exact displayed value, and defaults to 4 lives. We'll allow a range from 1-99,
like the TF2 hack. And the display subroutine (there is one now!) is at
$6c64.
btst #$2,D0 check joystick left
beq notleft branch if not
subq.w #1,$fffc.w decrement count
cmpi.w #$FFFF,$fffc.w test against FFFF (wraparound)
bne nowrap1 no need to wrap
move.w #$62,$fffc.w store 98 (99 total)
nowrap1:
bra disp jump ahead to display
notleft:
btst #$3,d0 check joystick right
beq $6bfc not valid, just loop
addq.w #1,$fffc.w increment count
cmpi.w #$62,$fffc.w test against maximum
bls nowrap2
move.w #$0,$fffc.w store '0'
nowrap2:
disp:
bsr $6c64 display new value
bra $6bfc wrap around
This
assembles to this block of code, that we drop in at $6c16. It ends at $6c4B, we
have till $6c59.
6C16: 0800 0002 6714 5378 fffc 0c78 FFFF fffc
6606
31fc 0062 fffc 6018 0800 0003 67c6
5278 fffc 0c78 0062 fffc 6306 31fc
0000
fffc 611A 60B0
We also need to fix the display code before we can
test this, as it displays the wrong variable. Breakpointing at $6C64 quickly
showed the address to patch:
6c72: change "F348" to "FFFC"
And, it's
necessary to replace $6C64 with a function that adds 1 to the value. this
function is called on entry to the screen and when you change its value, and
there isn't much room around it. But, we have some bytes free above, and all we
need to do is sneak in an increment. The function looks like this:
move.w #$14,$F20C.w load column
move.w #$2F,$f20e.w load row (encoded somehow)
move.w $fffc.w,D0 load value (after hack!)
bra $4dce go print number
So,
we can just replace that bra with a branch to our code, increment d0, then go on
to 4DCE. At $6c4C (spare room from above). This uses to $6c51 (still 8 bytes
free).
addq #1,d0 increment value
bra $4dce go print
6C4C:
5240 6000 e17e
6c76: replace "e158" with "ffd6"
Save and test at this
point, and you should see lives instead of voice test, and be able to select
from 01 to 99. Pressing the button should do nothing. Note, it may start at a
strange value, or 0, and it won't have any effect yet. Also test that if you
exit config and come back in that it remembers the value.
Menu is fixed,
so there are two tasks left - initialize it with a meaningful value (4), and
make it actually take effect.
Initialization is easy - we already added
code that makes it set the weapons sound flag on. This is located at $7194, and
is just two instructions (a bset, and a branch). Unfortunately, immediately
after it is some related code, meaning we don't have enough room to insert the
new initialization code there.
So, heck with it. We'll just move the
startup code a little further down - the next free spot is at
$71DE:
bset #$0,$fffe.w enable weapon sounds
movei.w #$4,$fffc.w set default of 4 lives
bra $200 go to real entry point
71DE:
08f8 0000 fffe 31fc 0004 fffc 6000 9014
and change the entry
point:
0004: 0000 71DE
Load that, and when you enter the configuration
screen, lives should be set to 04 by default. All that is left now is to update
the code that initializes it at the beginning of a game! For that, set a memory
breakpoint on $f2b4, so we can see when it is loaded. For me, it came up right
after the stage select, at $0502. Disassembling the area found this code at
$4F2:
clr.w $f2bc.w
move.w $f34c.w,$f2be.w
move.w #$4,$f2b4.w <---- here it is
move.l #$7d0,$f2b6.w
It's
in a reasonable area and doing what we expected.. if this is really it, all we
need to do is change it from an immediate value, to our stored value. So "move.w
$fffc.w,$f2b4.w". Should be the same size instruction.
04Fc: Change "31fc
0004" to "31F8 FFFC"
Now load it up and DO NOT enter the config screen -
make sure you start with 4 lives. Reset, change to some other value, and make
sure you start with that.
Now, finally, before we declare it done, we
need to fix the checksum. Breakpoint on reading $18E and turn OFF autofix
checksum.
I get a value of $3bC8 in D0, so drop that at $18E, and make
sure it works.
You'll get some killer high scores with 99 lives, so no cheating on
contests. ;)
So,
that was a lot of work, but you can also see that just locking down a different
count would be easy - change the 0004 at $04fE to whatever you like, and you're
set. But it's nicer to have it configurable, isn't it?