-
Notifications
You must be signed in to change notification settings - Fork 2
HowTo Patch A Trackload Game
This is a brief tutorial on how to patch a trackloaded Amiga game to support FlashFloppy AutoSwap. This will apply to most arcade titles, whereas adventures and RPGs (among others) usually access the disk via AmigaDOS: the patching method for AmigaDOS will be covered in a separate tutorial.
The game we will deal with in this tutorial is Mortal Kombat, published by Acclaim in 1993. This is a typical trackloaded title (by which we mean it accesses the drive hardware directly rather than using the OS) using RNC PDOS disk format and RNC CopyLock for copy protection.
The first decision is whether to work from original disk images and 'crack' the game as part of patching it, or to work from ready-cracked disk images. Where the disk format is not AmigaDOS sectors (as in this case, which uses non-standard PDOS) cracking will involve converting the disk format to AmigaDOS, so that it can be stored in ordinary ADF disk images. Since someone respected (NOMAD/Fairlight) has done the conversion from PDOS to AmigaDOS, it makes sense to use the existing crack as a starting point: we can be pretty confident that the crack will be 100% and will not introduce any more bugs or problems than the original game (and will most likely have fewer!).
The first step is to set up your build environment. Download the AutoSwap source repository and build the Mortal Kombat patch by following the Wiki instructions. Note that you need only build the MortalKombat subfolder; you don't need to build all supported titles.
The full patch for Mortal Kombat is quite large, as it fixes some game bugs and makes various speed improvements to the track-load routine. Since I want to focus on AutoSwap specifically, I have developed a minimal patch which is what we will discuss here.
After following the build steps above to build the full patch, you can now inspect and build the minimal patch:
# cd flashfloppy-autoswap/MortalKombat/minimal_example
# make
You should be able to run the resulting disk images from a FlashFloppy DF0 and observe that there are no requests for manual disk swapping.
So we're all set, but where do you even begin? My general approach is to first work out what code I need to patch (the game loader(s), copy protection(s), disk-swap requester(s), etc), and then work out how to apply the patches.
A good method for determining what to patch is to run the game under emulation (which will generally mean some flavour of UAE). I suggest emulating a standard A500, Kickstart 1.3, with 0.5M chip and 0.5M slow (aka trapdoor RAM). Run the original cracked disk images (in folder MortalKombat/FL_Crack/) until a disk request is displayed. Now pause emulation and run the debug monitor (on E-UAE this is CTRL-C at the console, on WinUAE this is Shift-F12).
You will now find yourself running somewhere in expansion memory:
D0: 000000f2 D1: 00000000 D2: 00000000 D3: 00000000
D4: 0000ffff D5: 0000f000 D6: 00001000 D7: 0000ffff
A0: 00c082b6 A1: 00000b9e A2: 00c00300 A3: 00c07e7c
A4: 00bfd000 A5: 00bfe001 A6: 00dff000 A7: 0000034e
USP=00c014aa ISP=0000034e MSP=00000000 VBR=00000000
T=00 S=1 M=0 X=0 N=0 Z=1 V=0 C=0 IMASK=3
00c010c2 3f00 MOVE.W D0,-(A7)
next PC: 00c010c4
We can assume the game code has been loaded into expansion RAM, and we can save off that 512kB memory state for closer inspection:
# S game.bin c00000 80000
I always then convert the entire state file into a 68k disassembly. You can use any raw-binary disassembler for this, however I use my own disassembler contained within the Disk-Utilities repository -- so you may want to go grab that and:
# cd Disk-Utilities/m68k
# make disassemble
# <now put 'disassemble' on your PATH>
You can then disassemble the game code to a text file:
# disassemble game.bin 0 - 0 >game.txt
Load the text file into your favourite text editor. Also copy/paste the register state (shown above) from UAE into the top of the text file: that's your current execution context and we will work up the stack from there.
Take a look at the code at the current PC (c010c2 in my example). Note that the hex numbers at the start of the line are offsets into the memory dump: add c00000 to get in-memory addresses.
000010c2 3f00 move.w d0,-(sp)
000010c4 3039 00df f01e move.w intreqr,d0
000010ca 0240 0010 andi.w #10,d0
000010ce 6600 01a6 bne.w 1276
...
Start of a routine, messes with intreqr, it's probably an interrupt handler! Not very interesting so we will walk up the stack. Let's look at the stack in UAE:
# m 34e
0000034e 2004 00c0 032a 0000 0000 00c0 8322 00c0 ....*......."..
0000035e 6b2a 0000 0001 0000 03a8 0000 0000 0000 k*..............
Yes, 2004 is saved SR, and c0032a is the return PC. So let's take a look at the routine containing the address c0032a:
00000310 48e7 8080 movem.l d0/a0,-(sp)
00000314 41f9 00c0 829e lea.l c0829e,a0
0000031a 600a bra.b 0326
0000031c 48e7 8080 movem.l d0/a0,-(sp)
00000320 41f9 00c0 82b6 lea.l c082b6,a0
00000326 1010 move.b (a0),d0
00000328 b010 cmp.b (a0),d0
0000032a 67fc beq.b 0328 <-- PC here
0000032c 4cdf 0101 movem.l (sp)+,d0/a0
00000330 4e75 rts
See how in a full text file disassembly it is quite easy to see surrounding context and work out where the subroutine boundaries must lie. You should feel free to add newlines or comments into the text file as you go, to delimit subroutines and to record your findings so far.
So we're in a wait loop of some kind, it doesn't look very exciting. Let's walk up the stack again: skip the saved D0/A0 registers and the next return PC is c006b2a:
00006a80 6100 0102 bsr.w 6b84
00006a84 6b00 00fc bmi.w 6b82
00006a88 4e56 ffc0 link.w a6,#-40
00006a8c 48e7 4002 movem.l d1/a6,-(sp)
00006a90 4def 0008 lea.l 8(sp),a6
00006a94 41f9 00c0 832a lea.l c0832a,a0
00006a9a 4cd8 00ff movem.l (a0)+,d0-d7
00006a9e 48d6 00ff movem.l d0-d7,(a6)
00006aa2 4cd8 00ff movem.l (a0)+,d0-d7
00006aa6 48ee 00ff 0020 movem.l d0-d7,20(a6)
00006aac 70ff moveq #-1,d0
00006aae 7400 moveq #0,d2
00006ab0 6100 ee84 bsr.w 5936
00006ab4 2079 00c0 82e0 movea.l c082e0,a0
00006aba 6100 9b62 bsr.w 061e
00006abe 41fa 0106 lea.l 6bc6(pc),a0
00006ac2 4cd7 4002 movem.l (sp),d1/a6
00006ac6 0641 0030 addi.w #30,d1
00006aca 1141 000f move.b d1,f(a0)
00006ace 4279 00c0 a95e clr.w c0a95e
00006ad4 4279 00c0 a95c clr.w c0a95c
00006ada 6100 f7dc bsr.w 62b8
00006ade 41fa 00f7 lea.l 6bd7(pc),a0
00006ae2 6100 f7d4 bsr.w 62b8
00006ae6 41fa 0102 lea.l 6bea(pc),a0
00006aea 6100 f7cc bsr.w 62b8
00006aee 2079 00c0 82e4 movea.l c082e4,a0
00006af4 2279 00c0 82cc movea.l c082cc,a1
00006afa 6100 00b2 bsr.w 6bae
00006afe 1039 00c0 82b8 move.b c082b8,d0
00006b04 0200 0020 andi.b #20,d0
00006b08 13c0 00c0 6c1e move.b d0,c06c1e
00006b0e 08b9 0005 00c0 bclr.b #5,c082b8
00006b14 82b8
00006b16 41fa 00e6 lea.l 6bfe(pc),a0
00006b1a 203c ffff 0000 move.l #ffff0000,d0
00006b20 7402 moveq #2,d2
00006b22 6100 ee20 bsr.w 5944
00006b26 6100 97f4 bsr.w 031c
00006b2a 6100 9736 bsr.w 0262 <-- PC here
00006b2e 67f6 beq.b 6b26
00006b30 4cd7 4002 movem.l (sp),d1/a6
00006b34 614e bsr.b 6b84
00006b36 6aee bpl.b 6b26
00006b38 6100 97e2 bsr.w 031c
00006b3c 6100 9724 bsr.w 0262
00006b40 66f6 bne.b 6b38
00006b42 103a 00da move.b 6c1e(pc),d0
00006b46 8139 00c0 82b8 or.b d0,c082b8
00006b4c 70ff moveq #-1,d0
00006b4e 7400 moveq #0,d2
00006b50 6100 ede4 bsr.w 5936
00006b54 2079 00c0 82cc movea.l c082cc,a0
00006b5a 2279 00c0 82e4 movea.l c082e4,a1
00006b60 6100 004c bsr.w 6bae
00006b64 6100 9afe bsr.w 0664
00006b68 4cd7 4002 movem.l (sp),d1/a6
00006b6c 41ee ffc0 lea.l -40(a6),a0
00006b70 43e8 0020 lea.l 20(a0),a1
00006b74 70ff moveq #-1,d0
00006b76 7402 moveq #2,d2
00006b78 6100 edca bsr.w 5944
00006b7c 4cdf 4002 movem.l (sp)+,d1/a6
00006b80 4e5e unlk a6
00006b82 4e75 rts
A relatively massive subroutine! It's not so easy to work out where it starts,
but note the immediately preceding instruction at offset 6a7c is a BRA.w. You
can also search for 6a80
in the text file and find several callers (both
as BSR 6a80
and JSR c06a80
: don't get confused by this, the BSRs are
PC-relative as if the expansion memory was based at 0x0).
So this looks like the disk-swap routine, and look at that first BSR: depending on how it sets the condition codes, this whole subroutine gets skipped. Could this be the "check disk id" subroutine?
If you look at the code at 6b84 and follow the BSR chain down a few steps, you should find yourself at 6de8, 6ebe, 6ed8, 6f16, 6f26. Find the subroutine boundaries and add short comments as you go.
The routine at 6f26 is interesting. It saves all registers, it loads up custom register addresses into registers, and sets up a stack frame. This looks like a dropped-in blob of code. And indeed if you search a little further into the text file, you will find references to disk registers: dsklen, dsksync, etc. We have reached the sector loader!
Okay, so: we have a suspected disk-swap routine at 6a80. Its first invocation is down a path which reads from disk (if you paid attention it looks like registers d1-d2 may get set to read one sector at offset 0x18, which sounds like a disk ID and/or file table). Let's look at the callers to 6a80: you should find them at 699e, 6efc, ab8a, and 16004. And note how all of them set up d1 to a small integer: a disk number?
A sensible path then will be to replace the bulk of this routine with a call to the FlashFloppy AutoSwap code. We could BSR to AutoSwap at 6a88, and then BRA back to the subroutine start (6a80). We will have the disk number in d1 on entry to AutoSwap.
Next question: Where do we put our patch code? In some games this can be a real problem, it needs to be placed in spare room on disk and in memory. A good place can be bypassed copy protection routines.
This Tutorial is Work In Progress: I will update it with the remaining text in the next week or two!