{"id":3197,"date":"2026-09-22T02:41:24","date_gmt":"2026-09-22T02:41:24","guid":{"rendered":"https:\/\/cml-a.com\/content\/?p=3197"},"modified":"2026-09-22T05:02:47","modified_gmt":"2026-09-22T05:02:47","slug":"contra-iii-ending-rom-hack-contd","status":"publish","type":"post","link":"https:\/\/cml-a.com\/content\/2026\/09\/22\/contra-iii-ending-rom-hack-contd\/","title":{"rendered":"Contra III Ending ROM Hack, cont'd"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This is a followup to this post: <a href=\"https:\/\/cml-a.com\/content\/2026\/09\/15\/contra-iii-ending-rom-hack\/\">https:\/\/cml-a.com\/content\/2026\/09\/15\/contra-iii-ending-rom-hack\/<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"296\" src=\"https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-13-1024x296.png\" alt=\"\" class=\"wp-image-3206\" srcset=\"https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-13-1024x296.png 1024w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-13-1536x445.png 1536w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-13-300x87.png 300w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-13-767x222.png 767w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-13.png 1983w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Similar to the first change, we're not dealing with easy, findable numerical quantities like \"health points\" or \"experience points\". <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As a shot in the dark, I used the debugger to break into a random spot as the ending screen was <em>about <\/em>to fade out and took a gigantic CPU log up until the first identifiable place where the fadeout started.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Despite this being a seemingly short period of time it created a bunch of log files, like 40 of them, 74000 lines each...<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"908\" height=\"733\" src=\"https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-14.png\" alt=\"\" class=\"wp-image-3209\" srcset=\"https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-14.png 908w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-14-300x242.png 300w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-14-767x619.png 767w\" sizes=\"auto, (max-width: 908px) 100vw, 908px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">It's likely to be a superset of what we need, although I can't even be sure of that.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From the logs, I did quickly find a spot we can hard lock. As in, replace two bytes of code with<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BRA $FE<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It's likely that this transcript contains<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>a bunch of game logic<\/li>\n\n\n\n<li>a one-time \"should we transition the screen\" check with a branch<\/li>\n\n\n\n<li>a bunch of VBlank handlers<\/li>\n\n\n\n<li>things to play sound and advance the animations<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I made a tool to do this:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/github.com\/clandrew\/logtools\/blob\/main\/FindCodeExecutedOnce\/FindCodeExecutedOnce.cpp\">https:\/\/github.com\/clandrew\/logtools\/blob\/main\/FindCodeExecutedOnce\/FindCodeExecutedOnce.cpp<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The repo is here: <a href=\"https:\/\/github.com\/clandrew\/logtools\">https:\/\/github.com\/clandrew\/logtools<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The tool does exactly what it sounds like, tracks a hit count for each program counter:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct CodeCoverage\n{\n    wchar_t PC&#91;6];\n    int HitCount;\n};\nstd::vector&lt;CodeCoverage&gt; coverages;\nvoid MarkCodeCoverage(wchar_t pc0, wchar_t pc1, wchar_t pc2, wchar_t pc3, wchar_t pc4, wchar_t pc5)\n{\n    CodeCoverage* pCoverage = nullptr;\n    for (size_t i = 0; i &lt; coverages.size(); ++i)\n    {\n        if (coverages&#91;i].PC&#91;0] == pc0 &amp;&amp;\n            coverages&#91;i].PC&#91;1] == pc1 &amp;&amp;\n            coverages&#91;i].PC&#91;2] == pc2 &amp;&amp;\n            coverages&#91;i].PC&#91;3] == pc3 &amp;&amp;\n            coverages&#91;i].PC&#91;4] == pc4 &amp;&amp;\n            coverages&#91;i].PC&#91;5] == pc5)\n        {\n            pCoverage = &amp;coverages&#91;i];\n            break;\n        }\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then, it reports all instances where the hit count was 1:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    for (size_t i = 0; i &lt; coverages.size(); ++i)\n    {\n        if (coverages&#91;i].HitCount == 1)\n        {\n            wprintf(L\"Found code this executed once, at PC=%c%c%c%c%c%c.\\n\",\n                coverages&#91;i].PC&#91;0], coverages&#91;i].PC&#91;1], coverages&#91;i].PC&#91;2], coverages&#91;i].PC&#91;3], coverages&#91;i].PC&#91;4], coverages&#91;i].PC&#91;5]);\n        }\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When I ran this tool on the logs, it found, basically, 5 results:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Found code this executed once, at PC=02FCD2.\nFound code this executed once, at PC=02FCD5.\nFound code this executed once, at PC=02FCD7.\nFound code this executed once, at PC=02FCD9.\n...(clipped for brevity)...\nFound code this executed once, at PC=02FD64.\nFound code this executed once, at PC=02FD67.\n\nFound code this executed once, at PC=1DB401.\nFound code this executed once, at PC=1DB404.\nFound code this executed once, at PC=1DB407.\n\nFound code this executed once, at PC=00BF6E.\nFound code this executed once, at PC=00BF71.\n\nFound code this executed once, at PC=04E2E4.\t; Ruled out; too late\nFound code this executed once, at PC=04E2E7.\nFound code this executed once, at PC=04E2E9.\nFound code this executed once, at PC=04E2EB.\nFound code this executed once, at PC=04E2ED.\nFound code this executed once, at PC=04E2EF.\n\nFound code this executed once, at PC=00EA91.\t; Ruled out; too late\nFound code this executed once, at PC=00EA94.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$02\/FCD0 D0 37       BNE $37    &#91;$FD09]   \n$02\/FCD2 20 0E FD    JSR $FD0E  &#91;$02:FD0E]  <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">to<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$02\/FCD0 80 37       BRA $37    &#91;$FD09]      \n$02\/FCD2 20 0E FD    JSR $FD0E  &#91;$02:FD0E]  ; No longer executed<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">However, this had no observable effect on the ending. It must be a false positive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Moving onto the second candidate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$1D\/B3FF 10 08       BPL $08    &#91;$B409]      ; This branch is not-taken once\n$1D\/B401 A9 00 01    LDA #$0100              ; Executed once\n$1D\/B404 8D 86 16    STA $1686  &#91;$05:1686]   ; Executed once\n$1D\/B407 F6 12       INC $12,x  &#91;$00:0912]   ; Executed once    <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Based on the control flow and the time at which it happens, this is a good candidate. <br>So I changed<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$1D\/B3FF 10 08       BPL $08    &#91;$B409]  <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">to<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$1D\/B3FF 80 08       BRA $08    &#91;$B409]      <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">so that it is always taken.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And voila, that hangs at the ending, with the looping animation, as needed:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"621\" src=\"https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-15-1024x621.png\" alt=\"\" class=\"wp-image-3210\" srcset=\"https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-15-1024x621.png 1024w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-15-300x182.png 300w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-15-767x465.png 767w, https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/image-15.png 1039w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"330\" height=\"288\" src=\"https:\/\/cml-a.com\/content\/wp-content\/uploads\/2026\/09\/Contra33.gif\" alt=\"\" class=\"wp-image-3212\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you want to apply this change yourself, you can use the PAR code <strong>1DB3FF80<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Or if you want this change plus the changes from <a href=\"https:\/\/cml-a.com\/content\/2026\/09\/15\/contra-iii-ending-rom-hack\/\">this earlier<\/a> post in a patch, you can download them all in a patch here:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/cml-a.com\/content\/data\/ContraIII.ips\">https:\/\/cml-a.com\/content\/data\/ContraIII.ips<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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&#8217;s a minor functional thing I didn&#8217;t like about the game. I don&#8217;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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[150,152,176,178],"class_list":["post-3197","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-retro","tag-reverse-engineering","tag-snes","tag-software-development-project"],"_links":{"self":[{"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/posts\/3197","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/comments?post=3197"}],"version-history":[{"count":14,"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/posts\/3197\/revisions"}],"predecessor-version":[{"id":3219,"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/posts\/3197\/revisions\/3219"}],"wp:attachment":[{"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/media?parent=3197"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/categories?post=3197"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cml-a.com\/content\/wp-json\/wp\/v2\/tags?post=3197"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}