📅September 22nd, 2026
This is a followup to this post: https://cml-a.com/content/2026/09/15/contra-iii-ending-rom-hack/
I playtested the hack and so far, so good. Still there's a minor functional thing I didn't like about the game. I don't like how, when you get to the ending screen, it stays there for no time at all then restarts you right back into a new game.

That's suitable for an arcade game, it's not desirable for a console game. At least for games of this generation, I'm used to them simply hanging at the ending screen until you reboot the system. This is on console, so it'd feel more appropriate to do that. Since I already had the debugger open, I took a look at making this change.
Similar to the first change, we're not dealing with easy, findable numerical quantities like "health points" or "experience points".
But in fact, this task of "changing the control flow of the ending" is even harder because we don't even have a given piece of memory like "difficulty level" to breakpoint on. Instead we're dealing with, like, pure control flow.
As a shot in the dark, I used the debugger to break into a random spot as the ending screen was about to fade out and took a gigantic CPU log up until the first identifiable place where the fadeout started.
Despite this being a seemingly short period of time it created a bunch of log files, like 40 of them, 74000 lines each...

It's likely to be a superset of what we need, although I can't even be sure of that.
From the logs, I did quickly find a spot we can hard lock. As in, replace two bytes of code with
BRA $FE
to spin forever. Technically yes this will hang the game at the ending. But then the game doesn't show the nice clouds or crowd animation, it looks very janky since it literally just freezes. The goal really is to have it keep showing the animations.
There are a couple ways to go about this. My hunch is to go kind of black box about this, and just do dumb analysis of the control flow to get a surgical change, don't exhaustively try to reverse engineer the whole transcript.
It's likely that this transcript contains
- a bunch of game logic
- a one-time "should we transition the screen" check with a branch
- a bunch of VBlank handlers
- things to play sound and advance the animations
The one-time check is the piece that we need to find, and most likely exists. Obviously if it doesn't then we can revisit that. Since it will be a one-time check, we can just scrape the logs for all the PCs that are executed one time.
I made a tool to do this:
https://github.com/clandrew/logtools/blob/main/FindCodeExecutedOnce/FindCodeExecutedOnce.cpp
The repo is here: https://github.com/clandrew/logtools
The tool does exactly what it sounds like, tracks a hit count for each program counter:
struct CodeCoverage
{
wchar_t PC[6];
int HitCount;
};
std::vector<CodeCoverage> coverages;
void MarkCodeCoverage(wchar_t pc0, wchar_t pc1, wchar_t pc2, wchar_t pc3, wchar_t pc4, wchar_t pc5)
{
CodeCoverage* pCoverage = nullptr;
for (size_t i = 0; i < coverages.size(); ++i)
{
if (coverages[i].PC[0] == pc0 &&
coverages[i].PC[1] == pc1 &&
coverages[i].PC[2] == pc2 &&
coverages[i].PC[3] == pc3 &&
coverages[i].PC[4] == pc4 &&
coverages[i].PC[5] == pc5)
{
pCoverage = &coverages[i];
break;
}
}
Then, it reports all instances where the hit count was 1:
for (size_t i = 0; i < coverages.size(); ++i)
{
if (coverages[i].HitCount == 1)
{
wprintf(L"Found code this executed once, at PC=%c%c%c%c%c%c.\n",
coverages[i].PC[0], coverages[i].PC[1], coverages[i].PC[2], coverages[i].PC[3], coverages[i].PC[4], coverages[i].PC[5]);
}
}
In theory, this sort of thing, it'd be possible to create a crazy flexible function tool to do it. Ultimately you'll hit a wall with the tool so honestly I think it's better to write code to operate on logs and ROMs in general.
When I ran this tool on the logs, it found, basically, 5 results:
Found code this executed once, at PC=02FCD2.
Found code this executed once, at PC=02FCD5.
Found code this executed once, at PC=02FCD7.
Found code this executed once, at PC=02FCD9.
...(clipped for brevity)...
Found code this executed once, at PC=02FD64.
Found code this executed once, at PC=02FD67.
Found code this executed once, at PC=1DB401.
Found code this executed once, at PC=1DB404.
Found code this executed once, at PC=1DB407.
Found code this executed once, at PC=00BF6E.
Found code this executed once, at PC=00BF71.
Found code this executed once, at PC=04E2E4. ; Ruled out; too late
Found code this executed once, at PC=04E2E7.
Found code this executed once, at PC=04E2E9.
Found code this executed once, at PC=04E2EB.
Found code this executed once, at PC=04E2ED.
Found code this executed once, at PC=04E2EF.
Found code this executed once, at PC=00EA91. ; Ruled out; too late
Found code this executed once, at PC=00EA94.
These represent 5 pieces of code, since the program counters from the same control flow are grouped together. The two ones at the bottom I was able to rule out quickly by hard locking them and seeing that the fade effect had already started. So it wouldn't be those.
That leaves the first three. The first one was a big long section. As a test, I changed it to get skipped over, as in changing
$02/FCD0 D0 37 BNE $37 [$FD09]
$02/FCD2 20 0E FD JSR $FD0E [$02:FD0E]
to
$02/FCD0 80 37 BRA $37 [$FD09]
$02/FCD2 20 0E FD JSR $FD0E [$02:FD0E] ; No longer executed
However, this had no observable effect on the ending. It must be a false positive.
Moving onto the second candidate:
$1D/B3FF 10 08 BPL $08 [$B409] ; This branch is not-taken once
$1D/B401 A9 00 01 LDA #$0100 ; Executed once
$1D/B404 8D 86 16 STA $1686 [$05:1686] ; Executed once
$1D/B407 F6 12 INC $12,x [$00:0912] ; Executed once
Based on the control flow and the time at which it happens, this is a good candidate.
So I changed
$1D/B3FF 10 08 BPL $08 [$B409]
to
$1D/B3FF 80 08 BRA $08 [$B409]
so that it is always taken.
And voila, that hangs at the ending, with the looping animation, as needed:


Since this stuff can be a bit hairy with unintended side effects I did a couple full playtests of the game to confirm it looks good.
If you want to apply this change yourself, you can use the PAR code 1DB3FF80.
Or if you want this change plus the changes from this earlier post in a patch, you can download them all in a patch here:
https://cml-a.com/content/data/ContraIII.ips
You can apply the patch using a tool such as LunarIPS or your emulator may apply it automatically if it's in a folder with a ROM with the same name. The patch is applied to the USA version SNES ROM.