Game objectives
- Emulator used: BizHawk 2.9.1 (SubNesHawk core)
- Aims for fastest time
- Corrupts memory
This run beats NES "Kaettekita! Gunjin Shogi: Nanya Sore!?" under a second with the DPCM exploit.
Note: I don't know whether this movie syncs on a real console, but I think theoretically you can beat this game like this way on a real console.
Note: There are no sound in the ending, possibly due to uninitialized RAM state (I will describe the details later).
I also made another movie for zero-initialized RAM, where the game plays sound correctly (encode).
I don't know much about initial RAM state, sorry.
About the game
"Kaettekita! Gunjin Shogi: Nanya Sore!?" is the only Gunjin Shogi game for NES.
There are five opponents, but all of them seem to have the same AI.
When you win three times against each of them, the last opponent will appear. When you beat the last opponent, the ending starts.
Comments
DPCM exploit
In this game, you can easily cause a stack overflow by continuing to stall the input processing routine with subframe inputs.
And, this game doesn't repurpose the stack page for other variables.
So, the remaining problem is a setup for a credit warp.
The ending routine is located at $E9B0. The goal is let the program counter jump there.
It is possible by taking some detours, though it is difficult to directly write the address as a return address.
First, when a NMI occurs, the stack grows as shown below (the NMI handler pushes A, X, Y registers in this order):
+---+---+---+---+----------+ | Y | X | A | P | rti_addr | +---+---+---+---+----------+
And, when the input processing routine stalls, there are more stuffs on the stack:
+------+------+---------------+------+------+---+---+---+---+----------+ | 0xCA | 0xFD | player2_input | 0x54 | 0xFF | Y | X | A | P | rti_addr | +------+------+---------------+------+------+---+---+---+---+----------+
So, when the stack overflows, you can let the program counter jump to $xxFD by overwriting the stack as below:
+------+----------------------+ | P | rti_addr | +------+----------------------+ ^ | +------+------+---------------+ | 0xCA | 0xFD | player2_input | +------+------+---------------+
I let the program counter jump to $FD by setting player2_input to 0.
$FD is the mirror variable of PPU_SCROLL x ($2005), and it's value is 0. This is
brk
instruction.
On IRQ/BRK, this game simply returns with rti
. So, the program counter proceeds to $FF (note that brk
instruction skips the next byte).
$FF is the mirror variable of PPU_CTRL ($2000), and it's value is 0x90. This is
bcc
instruction.
Its operand is fetched from $0100, and you can manipulate this while you are corrupting the stack.
I write 0xF4 to $0100. So, the program counter branches to $F5 (note that the carry flag is false, because I copied 0xCA to the status register on the stack).
$F5-$F8 is the input buffer. $F5-$F6 contains the edge inputs of player1/player2, and $F7-F8 contains the raw inputs of player1/player2.
I write
[0x20, 0xB0, 0xE9]
here. This is jsr $E9B0
.
(This is possible because 0x20 is a "subset" of 0xE9, considering a byte as a bitset.)
The game executes this code, and the program counter jumps to the ending routine.
By the way, the ending routine never returns, so you don't have to worry about after the jsr
instruction.
When you stall the input processing routine once, the stack will grow at most by 11 bytes. But, you can manipulate the consumed amount of the stack by adjusting the timing of NMI, using player2 inputs. (This game compares the inputs of player1/player2 separately, and the consumed CPU cycle differs slightly for each.)
Uninitialized RAM
On boot, This game doesn't initialize RAM so much, and it reads some uninitialized RAM (even without DPCM exploit).
In this run, I think the game reads the uninitialized RAM below:
addr | description |
---|---|
$17 | Some counter. Incremented by NMI handler without initialization. |
$23 | OAM DMA flag. |
$2A | Input buffer. This will have no problem. |
$2B | Input buffer. |
$F5 | Input buffer. |
$F6 | Input buffer. |
$F7 | Input buffer. Read as "previous input" without initialization. |
$F8 | Input buffer. Read as "previous input" without initialization. |
$075D | Variable for audio driver, probably. |
$075E | Variable for audio driver, probably. |
Especially, $075D and $075E will be problematic. Probably these are variables for the audio driver, and they have initial value 0xFF on NesHawk. I think this is the reason of no sound in the ending.
Memory map
addr | type | description |
---|---|---|
$F5 | u8 | player1 edge input |
$F6 | u8 | player2 edge input |
$F7 | u8 | player1 raw input |
$F8 | u8 | player2 raw input |
$FC | u8 | mirror variable of PPU_SCROLL y ($2005) |
$FD | u8 | mirror variable of PPU_SCROLL x ($2005) |
$FE | u8 | mirror variable of PPU_MASK ($2001) |
$FF | u8 | mirror variable of PPU_CTRL ($2000) |
$E9B0 | code | ending routine |
$FDBA | code | input processing routine |
$FF00 | code | NMI handler |
Inputs are in ABSTUDLR format.
Trivia
This game has weird bugs. Sometimes an enemy piece appears from nothing, and sometimes even an ally piece changes into an enemy piece. (example)
nymx: Claiming for judging.
nymx: This looks really good, just like your last submission. I didn't say this the last time, but this is proof that the emulator core is very accurate, as it mimics the DPCM hardware problem. I look forward to more of these runs. Great job.
Accepting.
despoa: Processing...