Michael Moffitt's Website

I like projects!




Fantasy Zone II DX Free Play Hack

Written 7/24/17


Fantasy Zone II DX is a new title released for the 1986 Sega System 16 hardware developed by M2, and released in 2008. Despite being developed for ancient arcade hardware, it was released to the public on the PlayStation 2 SEGA AGES collection, with a bundled emulator. Later, a 3DS port was made as well. Public arcade release or not, it was developed to run on a real board and a few were built for a Sega promotional event for the game.

This article isn't about running the game on the actual hardware, nor about the game itself. This is about the fact that the game only accepts credits for play with no DIP switches or menu to change this behavior (though there is a test menu). Extra annoying point: If there are any credits in the machine, the intro story won't play at all! This means if you coin it up and leave it there, it'll just cycle the demo and title screen but won't play the story or intro music. A free play modification that plays on zero credits and shows the story is ideal.

This modification does this, and still supports the intro story.


In order to add Free Play to a game that doesn't support it, there are a few constraints:
* When Start is pressed for either player, the game should begin normally
* When the game is not in use, the normal attract sequence should run (as if no coins are inserted)

Games like Pac-Man and Galaga have Free Play, but they just sit on the static "PRESS START" screen. This is bad for operators as well as collectors, as the game no longer shows the demo to illustrate the game, and the screen will slowly burn in with this PRESS START text prominently in the center. This sort of Free Play is not the desired type.

I attacked this problem piecewise; I'll outline each section and describe how the solution was obtained.

Starting a game without coins
Starting out fresh, it seems overwhelming, as it's unreasonable to expect to pick through all of the game's code and RAM looking for something useful. System 16 games are too big for that approach.

The first thing I did was set a watch point on the address to which the coin and start button inputs are read, which is $C41001. Using the mame debugger, a read watchpoint was put on this address with a WORD size:


wpset 0xC41001,2,r


I found a routine that would read inputs, and diff them with those of the previous frame, and store them in RAM. Button presses were stored starting at $228946 as byte-sized bitfields. I set a read watchpoint on this address next.

This triggered breaks in a few places. The one that stood out to me was a small subroutine that checked for P1 and P2 start buttons. After checking a few times I saw that D1 contained the number of credits inserted at the entry point.



$02EEE2 demoStartChk:
move.b ($228946).l, d0 ; Read start buttons
btst #4, d0 ; Check P1 start (bit 4)
beq.s noP1Press ; If not pushed, check P2
tst.w d1 ; Check if credits != 0
bne.w demo1PStart ; If so, start a 1P game

noP1Press:
btst #5, d0 ; Check P2 start (bit 5)
beq.s noP2Press ; If not, do nothing
cmpi.w #1, d1 ; Compare credits with 1
bgt.w demo2PStart ; If >1, start 2P game

noP2Press:
...


To patch this routine, the conditional branches after credit comparison were made unconditional, resulting in this modified routine:



$02EEE2 demoStartChk:
move.b ($228946).l, d0 ; Read start buttons
btst #4, d0 ; Check P1 start (bit 4)
beq.s noP1Press ; If not pushed, check P2
tst.w d1 ; Check if credits != 0
bra.w demo1PStart ; Start a 1P game.

noP1Press:
btst $5, d0 ; Check P2 start (bit 5)
beq.s noP2Press ; If not, do nothing
cmpi.w #1, d1 ; Compare credits with 1
bra.w demo2PStart ; Start 2P game.

noP2Press:
...


I found that this routine only fired during the gameplay demo. I traced backwards from demoStartChk, and found that D1 was being loaded with the contents of $23A690, which contains the number of inserted credits. After setting a watchpoint on $23A690, I found similar routine that reads the credit count at the title screen:



$0055D2 titleCheckCreds:
lea $1C(sp), sp
cmp.w ($23A690).l, d0 ; Check for changes in credits
beq.s checkInputs ; Skip if no change
tst.w d0 ; If previous credits were zero...
beq.s firstCreditIns ; Trigger "first credit" animation

checkInputs:
move.b ($228946).l, d0 ; Read start buttons
btst #4, d0 ; Check P1 start (bit 4)
beq.s noP1Press ; If not try P2
tst.w ($23A690).l ; Credits != 0?
bne.s title1PStart ; Start a 1P game if so.

noP1Press:
btst #5, d0 ; Is P2 start pushed?
beq.w noP2Start ; If not, abort
cmpi.w #1, ($23A690).l ; Check credits against 1
ble.w noP2Start ; If <=1, exit
bra.w title2PStart ; Start a 2P game otherwise.

noP2Start:
...


Changing this function was similar. The P1 start check was changed in the same way. The branch was made unconditional. For P2, the nonzero check's branch after checking the credit count was simply removed by replacing it with NOP instructions, so that it always falls through to the title2PStart branch. The subroutine then looked like this:


$0055D2 titleCheckCreds:
lea $1C(sp), sp
cmp.w ($23A690).l, d0 ; Check for changes in credits
beq.s checkInputs ; Skip if no change
tst.w d0 ; If previous credits were zero...
beq.s firstCreditIns ; Trigger "first credit" animation

checkInputs:
move.b ($228946).l, d0 ; Read start buttons
btst #4, d0 ; Check P1 start (bit 4)
beq.s noP1Press ; If not try P2
tst.w ($23A690).l ; Credits != 0?
title1PStart ; Start a 1P game.

noP1Press:
btst #5, d0 ; Is P2 start pushed?
beq.w noP2Start ; If not, abort
cmpi.w #1, ($23A690).l ; Check credits against 1
nop ; Fall through to 2P game start.
nop
bra.w title2PStart ; Start a 2P game.

noP2Start:
...


Finally, there is the story sequence, which has yet another routine that checks $23A690:



$02C20C storyCheckCreds:

; This block here checks if there is a credit inserted, and will
; return to the title screen if so. a0 + $228 looks like it holds
; the previously known credit count and is updated for comparison.
movea.l $28(a5), a0
move.w ($23A690).l, d0 ; Credit count
cmp.w $228(a0), d0 ; Compare to prev
beq.s checkInputs ; Nothing new, check inputs
move.w #9, $A(a5) ; Change game state probably
clr.w 8, (a5)
move.w ($23A690).l, $228(a0) ; Update coin copy

checkInputs:
move.b ($228946).l, d0 ; Read inputs
btst #4, d0 ; P1 button?
beq.s noP1Press ; Nope, try P2
tst.w ($23A690).l ; Nonzero credit test
bne.w story1PStart ; Start a 1P game if credits != 0

noP1Press:
btst #5, d0 ; P2 button?
beq.w noP2Start ; Nope, abort
cmpi.w #1, ($23A690).l ; Check for >1 credits
ble.w noP2Start ; Abort if not enough
bra.w story2PStart ; Start a 2P game if >1 credits


Following the same pattern as the title screen, the fix is simple. I was concerned that the game disabled the story mode when a game start was possible because there might be a problem, but it works fine from what I can tell.:



$02C20C storyCheckCreds:
movea.l $28(a5), a0
move.w ($23A690).l, d0 ; Credit count
cmp.w $228(a0), d0 ; Compare to prev
bra.s checkInputs ; Check inputs
move.w #9, $A(a5)
clr.w 8, (a5)
move.w ($23A690).l, $228(a0)

checkInputs:
move.b ($228946).l, d0 ; Read inputs
btst #4, d0 ; P1 button?
beq.s noP1Press ; Nope, try P2
tst.w ($23A690).l ; Nonzero credit test
bra.w story1PStart ; Start a 1P game

noP1Press:
btst #5, d0 ; P2 button?
beq.w noP2Start ; Nope, abort
cmpi.w #1, ($23A690).l ; Check for >1 credits
nop
nop
bra.w story2PStart ; Start a 2P game


Now we can start the game at any time. The core functionality of the mod is complete. However, the game will still subtract one or two credits when the game begins, which makes the coin counter go a little nuts displaying fun characters like / and . instead of staying at zero.

Changing the game cost to zero

Finding this one was a lot simpler. I set a watchpoint on writes to $23A690 (the credit count). That triggered during initialization (where it was zeroed out) as well as when a game was started with this little function:



; This function seems to be called whenever the number of credits
; is to be updated. The first argument is the amount to change the
; credits by.
$024A1A creditUpdate:
move.l d2, -(sp)
move.l 8(sp), d0 ; Fetch first argument into d0
move.w d0, d2 ; Copy argument into d2
ble.s subCoin ; Branch if change <= 0
pea (somewhere).w
jsr (another sub).l
addq.l #4, sp

subCoin:
move.w ($23A690).l, d0 ; Put credits in d0
add.w d2, d0 ; Apply change
move.w d0, ($23A690).l ; Write back credit count
...


In order to make the game truly free the argument for cost is zeroed out so this routine never changes the counter. Instead, d2 is cleared.



$024A1A creditUpdate:
move.l d2, -(sp)
move.l 8(sp), d0 ; Fetch first argument into d0
clr.w d2 ; Zero out cost
ble.s subCoin ; Branch if change <= 0
...


Now the game always shows "CREDIT 0" in the corner. That's great, and not untrue - but it should read "FREE PLAY" instead.

Changing credit text to Free Play"

Since the System 16B has a tilemap system, with 2 bytes per tile, it's up to the developer about how to store and display strings. It can either be a C-string (char[]) or it would be direct storage of tilemap data to be copied to the tilemap itself. Fortunately, the "CREDIT 0" string is stored as a C-string in ROM:



$03AAC2 CreditString:
dc.b 'CREDIT 0',0


This is a pretty obvious change:



$03AAC2 CreditString:
dc.b 'FREE PLAY',0


Sure enough, FREE PLAY was displayed. However, some naughty function was replacing the L with a ' ' and the Y with a '0'. It appears that the base string is drawn on the tilemap, and then a function checks the credit value and updates the plurality of credit by writing a space or 'S', as well as the single-digit credit count in the corner.

I found the text-printing function by setting a watchpoint on $03AAC2, checking for a 10-byte length to see who's reading this string. I found a function at $023312 that is used for general-purpose string printing on the tilemap. This little shred of it was actually responsible for modifying the tilemap:



movea.l #$410030, a3 ; a3 points to tilemap base
adda.l a0, a3 ; Offset by calculated position
move.b (a3), d1 ; Tile is written


Not only did this give me the tilemap base address, but with MAME's debugger I was able to proceed hit-by-hit and watch individual characters get drawn. By halting on the final character of "CREDIT 0" I was able to get a watchpoint for that exact tilemap location. Using that, I found who was writing '0' to the corner of the screen over my FREE PLAY text. Turns out it's part of that same credit update function from before:



$024A1A creditUpdate:
move.l d2, -(sp)
move.l 8(sp), d0 ; Fetch first argument into d0
move.w d0, d2 ; Copy argument into d2
ble.s subCoin ; Branch if change <= 0
pea (somewhere).w
jsr (another sub).l
addq.l #4, sp

subCoin:
move.w ($23A690).l, d0 ; Put credits in d0
add.w d2, d0 ; Apply change
move.w d0, ($23A690).l ; Write back credit count
btst #2, ($22366B).l ; Not sure what this is...
beq.s creditExit ; ...but it just early returns.

drawCreditsCount:
movea.w d0, a0 ; Not sure why a0 instead of dx??
moveq #9, d0 ; Compare credit count to 9
cmp.l a0, d0 ; using a0, for some reason...
bge.s nineOrLessCreds ;
moveaw.w #9, a0 ; Clamp credit count to 9

pluralizeCredit:
move.b #$93, ($410DFB).l ; Draw 'S' to pluralize credit

drawCredCount:
move.w a0, d0
addi.w #$70, d0 ; Calculate character (0 - 9)
ori.w #$8000, d0 ; Set some attributes
move.w d09, ($410DFE).l; Draw credit count to corner
bra.s creditExit ; and we're done.

nineOrLessCreds:
moveq #1, d0 ; Check if 1 is less
cmp.l a0, d0 ; than the credit count.
blt.s pluralizeCredit ; If so, draw 'S' and count
move.b #$60, ($410DFB).l ; Otherwise, draw ' ' after CREDIT
bra.s drawCred ; Draw the credit count.

creditExit:
move.l (sp)+, d2 ; Return... something
rts


This one I fixed by removing the writes to $410DFB and $410DFE. In retrospect, just having this function early return would have likely worked just fine, but I wasn't 100% sure that this didn't do anything else with some side effects, or might be entered midway at some other point, so the structure of it is retained along with any effects it might have. Who knows, maybe that return value is important. I'm not interested in optimizing this function that is only called during init.



pluralizeCredit:
nop
nop
nop
nop


drawCredCount:
move.w a0, d0
addi.w #$70, d0 ; Calculate character (0 - 9)
ori.w #$8000, d0 ; Set some attributes
nop ; but then do nothing
nop
nop

bra.s creditExit ; and we're done.

nineOrLessCreds:
moveq #1, d0 ; Check if 1 is less
cmp.l a0, d0 ; than the credit count.
blt.s pluralizeCredit ;
nop ; Don't do a damn thing about it
nop
nop
nop

bra.s drawCred ; Draw the credit count.


Now "FREE PLAY" shows up properly.The game is still flashing "Insert Coin" on the title and demo screens, though.

Flash "1 or 2 Player Start" instead of "Insert Coin"

Time to check for $23A690 checks again. A read watchpoint showed a routine that fired during the demo sequence:



$02EE44 demoCreditPrintCheck:
move.w ($23A690).l, d0 ; Read credit count
bne.w demoShowStart ; Show press start text if > 0




$02EF76 demoShowStart:
cmpi.w #1, d0 ; Is there only one credit?
beq.w demoSingleStart ; If so, only print 1P start
... ; Go on to print 1P/2P start


That press start routine decides to show "1 Player Start" or "1 or 2 Player Start" by checking if the credit count (d0) is 1. This naive implementation is great, because if credits are zero, that means it shows "1 or 2 Player Start". We don't even have to touch the routine:




$02EE44 demoCreditPrintCheck:
move.w ($23A690).l, d0 ; Read credit count
bra.w demoShowStart ; Show press start text


That leaves the title screen's flashing text. Yet another watchpoint on the credit count shows this:



$005766 titleStartMessage:
move.w ($23A690).l, d1 ; Read credit count
bne.w titleShowPressStart ; If nonzero show start prompt
...




$00604A titleShowPressStart:
cmpi.w #1, d1 ; Is it just one credit?
beq.w titlePrint1P ; Prompt P1 to begin
btst #5, d0 ; Set up call to printStatus
seq d0
ext.w d0
movea.w d0, a0
pea $E(a0)
bra.w titleDoP2Print
...




$00577E doTitleStatusPrint:
jsr printStatus


This is the function actually responsible for putting the text on the screen:



$023C2C PrintStatus
move.l 4(sp), d1 ; Message choice is in arg 0
move.l d1, d0
lsl.l #3, d0
sub.l d1, d0
lsl.l #3, d0
movea.l d0, a1
adda.l #$3891A, a1 ; Base tilemap layouts source
move.w 2(a1), d1
lsl.l $6, d1
andi.l #$3FFC0, d1
clr.l d0
move.w (a1), d0 ; Get metainfo from layout base
add.l d1, d0
movea.l d0, a0
adda.l d0, a0
adda.l #$410030, a0 ; Tilemap destination
addq.l #4, a1
move.w (a1), d0
beq.s printEarlyExit

printLoop:
addq.l #2, a1 ; destination++
move.w d0, (a0)+ ; Write tile
move.w (a1), d0 ; Fetch next file
bne.s printLoop

printEarlyExit:
rts



This function is a flexible one that can print one of many status messages. It is used by the title screen as well as the demo screen.

I have tried these arguments at sp+4 and made it work:
* 0 = Clear Insert coin (bottom)
* 1 = Insert coin (bottom)
* 2 = Game Over
* 3 = Clear Game Over
* 4 = Fantasy Zone II demo text
* 5 = Clear Fantasy Zone II demo text
* 6 = (C) Sega 1987
* 7 = clear sega text
* 8 = Insert Coin / Start (top)
* 9 = Clear Insert Coin / Start (top)
...
* F = 1 Player Start
* D = 1 or 2 Player Start

The mappings start at $03891A, and the first four bytes seem to describe length or some other metadata.

Anyway, to make it flash 1 or 2 Player Start, the code is modified as so:




$005766 titleStartMessage:
move.w ($23A690).l, d1 ; Read credit count
bra.w titleShowPressStart ; Show start prompt
...


This change should not strictly be necessary, but in case the credits change from some oversight on my part, this will ensure it shows P1/P2 start only.



$00604A titleShowPressStart:
cmpi.w #1, d1 ; Is it just one credit?
beq.w titlePrint1P ; Prompt P1 to begin
btst #5, d0 ; Set up call to printStatus
...


With that, all requirements are met.

Summary of Changes



# Show "Free Play" instead of "Credit 0"
0x03AAC2 = 0x4652454520504c4159 ("Free Play")
0x024A54 = 0x4E714E714E714E71 (nop nop nop nop)
0x024A66 = 0x4E714E714E71 (nop nop nop)
0x024A74 = 0x4E714E714E714E71 (nop nop nop nop

# Flash "1 or 2 Player Start" instead of "Insert Coin" at Title
0x00576C = 0x600008DC (bra.w +0x8DC)
0x00604E = 4E714E71 (nop nop)

# Coin insert doesn't reset title screen
0x0055E0 = 0x4E71 (nop)

# Coin insert doesn't transition from story -> title
0x02C21A = 0x6012 (bra.s +0x12)

# Game start doesn't subtract coin(s)
0x024A20 = 0x4282 (clr.l d2; d2 is cost argument)

# Skip credit check at title/high score
0x0055F4 = 0x602C (bra.s +0x2C game start @ 0x5622)
0x005606 = 0x4E714E71 (nop nop)

# Skip credit check during demo screen
0x02EEE2 = 0x600000B4 (bra.w +0xB4)
0x02EEF0 = 0x600000C4 (bra.w +0xC4)

# Skip credit check during story
0x02C240 = 0x600000E2 (bra.w +0xE2)
0x02C254 = 0x4E714E71 (nop nop)

# Display 1 or 2 players start instead of Insert Coin (center)
0x02EE4C = 0x60000128 (bra.w +0x0128 game start @ 0x2EF76)

Omake: Fix "H.MOMB" typo to show "H.BOMB"

This one's a very simple hack, but on the weapon select screen the Heavy Bomb weapon has a typo (or a bad dump? heh) that makes it say "H.MOMB". This is an easy fix: change $03ACA0 to 0x42 ('B'). The typo is kind of cute though, so I recommend not actually fixing it and leaving it as-is.

Double Omake: Rom Combining / Splitting
The board this game runs on has 8-bit ROM sockets, so two ROMs at minimum are needed for the 68000 program data. One ROM provides even bytes and the other provides odd bytes. These are "interleaved in hardware" in that they share the same addressa and control signals (except high/low byte select with /OE) and satisfy different halves of the data bus. In order to work with the dumped ROM images, they must be combined.

Here is a little C tool I threw together for combining or splitting interleaved files:



// bsplit - file interleave / deinterleaving tool
// Michael Moffitt 2017
#include
#include
#include

void split(const char *in_fname, const char *out_even_fname,
const char *out_odd_fname)
{
int c;
FILE *fin, *fout_odd, *fout_even;
fin = fopen(in_fname, "rb");
if (!fin)
{
fprintf(stderr, "Couldn't open %s.\n", in_fname);
return;
}
fout_odd = fopen(out_odd_fname, "wb");
if (!fout_odd)
{
fprintf(stderr, "Couldn't open %s.\n", out_odd_fname);
fclose(fin);
return;
}
fout_even = fopen(out_even_fname, "wb");
if (!fout_even)
{
fprintf(stderr, "Couldn't open %s.\n", out_even_fname);
fclose(fin);
fclose(fout_odd);
return;
}

do
{
c = fgetc(fin);
if (feof(fin))
{
break;
}
fputc(c, fout_even);

c = fgetc(fin);
if (feof(fin))
{
break;
}
fputc(c, fout_odd);
}
while (c != EOF);

fclose(fin);
fclose(fout_odd);
fclose(fout_even);
}

void combine(const char *in_even_fname, const char *in_odd_fname,
const char *out_fname)
{
int c;
FILE *fin_odd, *fin_even, *fout;
fout = fopen(out_fname, "wb");
if (!fout)
{
fprintf(stderr, "Couldn't open %s.\n", out_fname);
return;
}
fin_even = fopen(in_even_fname, "rb");
if (!fin_even)
{
fprintf(stderr, "Couldn't open %s.\n", in_even_fname);
fclose(fout);
return;
}
fin_odd = fopen(in_odd_fname, "rb");
if (!fin_odd)
{
fprintf(stderr, "Couldn't open %s.\n", in_odd_fname);
fclose(fin_even);
fclose(fout);
return;
}

do
{
c = fgetc(fin_even);
if (c != EOF)
{
fputc(c, fout);
}
c = fgetc(fin_odd);
if (c != EOF)
{
fputc(c, fout);
}
} while (c != EOF);

fclose(fout);
fclose(fin_even);
fclose(fin_odd);
}

int main(int argc, char **argv)
{
if (argc < 5)
{
printf("Usage: %s [op]\n", argv[0]);
printf(" split: s in out.even out.odd\n");
printf(" combine: c in.even in.odd out\n");
return 0;
}

if (argv[1][0] == 's')
{
split(argv[2], argv[3], argv[4]);
}
else if (argv[1][0] == 'c')
{
combine(argv[2], argv[3], argv[4]);
}

return 0;
}


I have built this tool with the output name "bsplit", which is used by two scripts.

This script is used to interleave the 8-bit ROMs and produce one flat 68000 program ROM image:



#!/bin/bash
./bsplit s fz2.bin _fz2.e _fz2.o

split _fz2.e -b 131072
mv xaa fz2.a7
mv xab fz2.a8
rm _fz2.e

split _fz2.o -b 131072
mv xaa fz2.a5
mv xab fz2.a6
rm _fz2.o



This script is used to split and bytesplit the flat 68000 image into the four 8-bit ROMs so that they may be tested:



#!/bin/bash
./bsplit c fz2.a7 fz2.a5 _fz2.lo
./bsplit c fz2.a8 fz2.a6 _fz2.hi

cat _fz2.lo _fz2.hi > fz2.bin
rm _fz2.lo
rm _fz2.hi


Back to main index


This website was generated using MicroLog.