After
doing the extra work to allow single life games in TF2 and TF3, I was a little
disappointed that TF4 doesn't allow it - going from 2 stock ships (3 lives) to 0
(99 lives). So, since I'd already found the menu code and it was above the
hack-protected (hopefully) area, I wondered about just changing it to work the
same as the other two.
When I last looked at TF4, I found the menu
settings for the music test around $FF1A, so the stock ship entry would probably
be around there, too. I disassembled the region to try and find a likely
candidate, expecting similar patterns to above. This didn't work, so I fell back
on the TF2 trick and dumped RAM a couple of times.
This identified $8C96
as the likely candidate, showing the same value as the screen did. The good news
was changing this value changed the menu correctly. The bad news was setting an
invalid value locked up the menu (perhaps because of the animation), meaning I
might need to do a little rewriting to display the numbers like the music test,
rather than the fancy spinning numbers.
Anyway, I could breakpoint on
writes to that address to find the menu code. Moving right stopped on $FE32, and
I disassembled the area to find a somewhat more complex block of code, but
eventually found the interesting parts starting at $FE1E:
moveq #$1,D4 a keypress is confirmed at this point - sets step to '1'
btst #$2,d0 test joystick left
beq notleft
moveq #$-1,d4 set step to -1 if it was left
It
then goes through some table lookups and subroutine calls that I presume have
something to do with the animation. Finally, we pick up again at
$FE78:
add.w d4,d6 add step to count
bpl nonegwrap branch if it didn't go below zero
moveq #$4,d6 wrap to high value of 4
bra valueset
nonegwrap:
cmp.w #$1,d6 check if we went to 1
bne notone
add.w d4,d6 add the step again (2->1 becomes 2->0, 0->1 becomes 0->2)
bra valueset
notone:
cmp.w #$5,d6 check for high value 5
bne valueset all tests done if this passed
moveq #$0,d6 wrap around to 0
valueset:
move.w d6,$8c96.w write the new value back out
Then
there is some similar code that again looks like something to do with the
animation.
So at the beginning, the hack looks pretty simple. Change the
high values, remove the skip over of '1' (which really means 2 lives). The
animation will likely break, badly, based on the earlier test, but we can do
that much. We'll also need to find (later) where 0 becomes
99.
Unfortunately, we can't use moveq with values as high as 99, so this
block needs a rewrite. Luckily we should have lots of space.
org $fe78
add.w d4,d6 add the step
bpl lowokay if it didn't wrap low, jump ahead
move.w #$62,d6 if it did, wrap around to 98
bra valueset value okay
lowokay:
cmp.w #$62,d6 check for high value
bls valueset branch if okay
clr.w d6 reset to zero if not
valueset:
move.w d6,$8c96.w write the value back
nop padding to match original code
This
looks like this - from $FE78 to $FE95
FE78: dc44 6a00 000a 3c3c 0062 6000
000c bc7c
0062 6300 0004 4246 31c6 8c96 4e71
This works as anticipated
- '1' displays as '0' (the lookup tables matched that), and going below zero or
above 4 crashes the system so badly the emulator messes up. ;)
So now we have to work out how to display the numbers like the music test does,
without the animation.
I already have the music test code at $ff1a for
selecting, so I took a look at how it displays the number. The music index was
stored in $8c98, then the code jumps to $ff76. This does a branch to $11ec8,
which appears to be a VDP function that waits for 2 vertical blanks, then
returns.
The rest of the function does appear to be a display
function:
lea $b000.w, a0 prepare VDP address
moveq #$f,d0 prepare mask
and.w $8c98.w,d0 get last digit
ori.w #$a000,d0 VDP attributes?
addi.w #$11,d0 character set offset?
moveq #$1c,d1 column 28?
moveq #$11,d2 row 17?
bsr $123b8 VDP character out
move.w $8c98.w,d0 get value again
and.w #$f0,d0 get second digit
lsr.w #4,d0 shift down
or.w #$a000,d0 rest same as above
addi.w #$11,d0
moveq #$1b,d0 except column 27
moveq #$11,d2
bsr $123b8
move.w $8c98.w,d0 but then what's this?
bsr $1231c could be title lookup - part I need is done anyway
So
the code appears to be hard-coded, inline. This particular code is BCD, which is
not really what I want.
Should be possible to overwrite the animation
code with something similar, then.
I decided, too, since TF2 and TF3 are
showing how many LIVES you get, that I would make Lightening Force be
consistent, and display 1-99, rather than 0-98.
org $fe96
bsr $11ec8 wait 2 vblanks
lea $b000.w, a0 prepare VDP address
moveq #0,d6 prepare d6
move.w $8c96.w,d6 make sure it's JUST a word
addq #1,d6 increment it for display
divu #10,d6 after storing it, divide by 10 - MSW=remainder, LSW=quotient
moveq #0,d0
move.w d6,d0
ori.w #$a000,d0 VDP attributes?
addi.w #$11,d0 character set offset?
moveq #$1b,d1 column 27
moveq #$0f,d2 row 15
bsr $123b8 VDP character out
swap d6
move.w d6,d0
ori.w #$a000,d0 VDP attributes?
addi.w #$11,d0 character set offset?
moveq #$1c,d1 column 28
moveq #$0f,d2 row 15
bsr $123b8 VDP character out
rts done
This
is placed right after the NOP above. The RTS is at $FED8, which this easily fits
in. I've included a BSR $11EC8, which as I mentioned is just a VDP delay,
because the code seems to like it.
FE96: 6100 2030 41f8 b000 7c00 3c38
8c96 5246
8cfc 000a 7000 3006 0040 a000 0640 0011
721b 740f 6100 24fc 4846
3006 0040 a000
0640 0011 721c 740f 6100 24e8 4e75
That overwrites the
second half of the animation (where it expands), we still need to eliminate the
first half, too. For that, we'll just jump over it from $FE32 to $fe78
org $fe32
bra $fe78
FE28:
replace "41fa" with "6044"
This now works, but, it spins very quickly. We
need to change the input from the continuous to the edge triggered one, and we
still need to make 0 mean 0. (Other values seem to work). We also need to fix
the entry to the configuration screen, which does not display right.
So
again, back to the input code. We know a direction is being set at $FE1E, so I
had work a little further back to see where it was coming from.
At $FE0E
there's this block:
move.w $8c82.w,d0 get joystick value
or.w $8c84.w,d0 merge joystick value 2
and.w #$c,d0 mask to just left and right
beq $fed8 branch if neither set
I
know from TF3 that there is probably a 'just pressed' and a 'held' value on the
joystick, and those are probably both of them (for whatever reason). So I just
need to see which is which. By loading up the RAM viewer to watch those values,
however, I made an even better discovery - at $8CBE is a joystick value which is
a keypress with delay then repeat - it's used for the menu up/down. But it
should do the trick here perfectly. Besides that, $8c82 was the current value,
and I couldn't tell from looking what $8C84 was. Breakpointing in the joystick
routine would tell me, but that seems unnecessary.
So all I want to do is
change the $8C82 to $8CBE. The OR doesn't seem to cause issues, so for now we'll
leave it.
$FE10: change "8C82" to "8CBE"
After that lucky break,
the next two issues just require watching the value at certain points - we need
to trap the entry to the configuration screen to draw the value correctly, and
we need to trap the game start to no longer remap 0 to 99. So, we set a
breakpoint on reading $8C96 (while NOT in config). This quickly showed that
memory location was changed a lot... $F056 appears to contain the persistent
copy of the configuration.
Using that, the entry display code looks like
this at $F9E0:
move.w $f056.w,d6
lea $feea.l,a0
move.w d6,d7
add.w d7,d7
move.w (a0,d7.w),d7
move.w #$8000,d0
moveq #$1c,d1
moveq #$f,d2
movea.l (a2,d7.w),a0
bsr $123cc
Most
of that, I believe, are lookups into the animation tables. Since we have a
lovely little display routine already at $fe96, we can just BSR to that. The
next code reloads A0 and D0, which our function uses, and we can just load D6 in
case it wants it later (I doubt it, but we have room).
org $f9e0
move.w $f056.w,d6 load d6
move.w d6,$8c96.w save it in the workspace
bsr $fe9A display ships (skipping over the vblank wait as the VDP is off)
move.w $f056.w,d6 reload d6 just in case
bra $fa02 apparent continuation code
F9E0:
3c38 f056 31c6 8c96 6100 04B0 3c38 f056
6010
With that working, all
that is left is making sure that 0 does not get translated to 99 (and making
sure 0 works as a single-ship game). Set the lives to 00 and exit config. Then
set the breakpoint (on $F056, now that we know better), then begin a
game.
This triggered after the selection screen, like in TF3, right
around A5E. The usual disassembly and look for sense happened. And the answer
was actually pretty clear at $a5a:
move.w $f056.w,$f2f0.w copy lives config to active lives count
bne notzero jump if not zero
move.w #$63,$f2f0.w set lives remaining to 99
notzero:
All
we need to do, then, is defeat this test and load - the simplest thing to do is
replace the move immediate with a copy of the move.w above it. But there is a
small problem.
Remember the security check we found that locked up TF4 on
collecting a 1-up? This code is right in the middle of that
range.
Unfortunately, there seems to be no way around it if I want this
behaviour. I'll be able to test the game through, at least, and hopefully there
are no other such checksums.
So, first, the patch is to replace at $A62:
"31fc0062" - replace with "31f8f056". That repeats the copy if its zero, no harm
done.
The checksum code is at $5cae. It's safest to fix the checksum in
case of other tests, rather than defeat the branch, so set a breakpoint at
$5cc6. Then you have to play till you get a 1-up (early one in the water stage).
The checksum is located at $15ff8, and is a full long. In my case, replace
"97BEF5C5" with "87abf5c9". This of course also affects the main
checksum.
After all these changes, the main checksum at $018E should be
set to "1Ae6".
After I've had a chance to run through each game, I'll post patches, but
I think I'm done for tonight ;)