User File #638398423526853571

Upload All User Files

#638398423526853571 - Super Mario Bros. ACE Total Control "Travelling Salesman" playaround

SMB1_ACE_ShareV1.tasproj
In 02:25.38 (100726 frames), 0 rerecords
1 comment, 227 downloads
Uploaded 1/3/2024 1:32 AM by OnehundredthCoin (see all 12)
Uses subneshawk in Bizhawk 2.9.1, this TAS begins with pre-set RAM. (would be obtained through a brief SMB3 TAS and then cartridge swapping)
This demonstration includes: Custom music, custom screens, bad jokes.
in an earlier Userfile, I demonstrated simply changing the game from world N-2 to 8-4, winning the game about a minute and 15 seconds after the console was turned on. This time I decided to play around with ACE as much as possible, utilizing the 2Kb on RAM to set up code which lets me write more code. I end up writing a nice loop allowing me to read the controller to determine what needs updated: APU registers, OAM, the Nametables, or perhaps nothing at all. With it I tell the story of what happens to Mario after saving the day, and according to myself it involves the Travelling Salesman Problem.
"Cool, so how does this work?"
Let's begin by explaining how this ACE Exploit works.
In Super Mario Bros., killing Bowser with fireballs will "Reveal his true form". In world one, he's really a goomba, in world 2, a buzzy beetle, and so on. When you parse this table out of bounds, some tomfoolery occurs. World 'N' is world 0x16, so we're replacing Bowser with the 23rd entry in a table 8 entries long. This results in Bowser becoming an object beyond the bounds on the object table! Regardless, the game loads that ID and jumps somewhere to determine how it should behave. It ends up jumping the PC to the end of the level loading routine, right at the part where existing sprites are all cleared out, and the "gameplay loop mode" is incremented. Normally, this would increment it from 2 (setting up a stage) to 3 (normal gameplay loop) but since it was already at 3, now it becomes 4 which, again, will parse a table out of bounds. When the game determines where to jump for "Gameplay loop mode 4" it ends up in open bus! (with 53 on the databus) This runs `SRE ($53),Y` and to make a long story short that ends up shifting address $000A to the right, and this new value is on the databus. Luckily for us, we can manipulate address $000A before killing bowser. If we set it to the value 0x80 (by simply holding the A button and *not* the B button) this will shift into 0x40, which will place an `RTI` instruction in open bus, jumping the PC to address $181.
Address $181 is not cleared when the game boots up, so if you were to write a payload there in another game, such as Super Mario Bros. 3, and swap cartridges without turning off the power, the code you write will still exist when killing Bowser in SMB1. Okay... so what code do I put there?
The code I write will begin by reading the controller twice and storing the values read at $c3 and $c2. This will be a pointer to where we write our own custom code. I read the controller once more, this time storing the value at $c1. This will be the payload length. I read the controller X times, where X is the payload length, storing the values read at where ($c2) points. Once all the bytes are written, I read the controller one more time to determine if we should restart the loop, change the pointer, and write something new. Otherwise, I read the controller 2 more times (forming a pointer) and jump there. This lets me execute the custom code I write.
Fun fact, I cannot use the NMI to wait for V-Blank. My solution is to use address $2002, which might've raised a few eyebrows. Reads from $2002 if on the wrong PPU cycle can cause a race-condition wherein the console returns a false-negative, stating the V-Blank has not occurred when in reality it has. My work around for this is to start each frame by reading the controller, and if in that input I press the right button, a function I wrote will waste precisely 1 CPU cycle, circumventing the race condition. I wrote a lua script to automate the process of pressing the right button, since it happens a lot.
I begin by writing code that let's me update the nametables. I clear them and draw a blue box. I spend a few frames writing a custom audio engine, but I decide to keep using SMB1's built in audio engine to finish playing the Bowser Defeated sound and the victory jingle. During this time I also update the nametable to add some humorous text. "Bowser is defeated" "Peace has returned to the kingdom" ... "Mario files for unemployment" "The kingdom assigns him the title of..." and now it's time for part 2.
Part 2: moving the screen and playing custom music.
The loop of code I wrote is essentially, "read controller to determine if we need to update audio. (if we do, read the controller to determine which channels need updating, then read the controller multiple times per channel) Read the controller to determine the horizontal scroll of the screen"
With my custom music player, I was able to extract data from famitracker, run it though a program I made (converting it into button presses) and play it in my TAS via pressing buttons! As the screen scrolls during this section (also done through pressing buttons)it reveals the title the kingdom has assigned Mario. he is now the Travelling Salesman! ("The NP-Hard game to TAS!")
Part 3: OAM
I modified the loop since I no longer need to scroll the screen. For here, I added some code to modify OAM data. This lets me make Mario run across the screen. The loop is now "Read the controller for the Race-Condition-Fix. Read the controller to determine audio channels that need changed. Read multiple times per changed audio channel. Read to determine graphical changes (the lower 3 bits are Nametable, OAM, and Scroll). Read for OAM changes if needed, 1 read to determine how many bytes, 1 read for the OAM offset, then x reads to fill in the data. If updating the nametable, this jumps to the main setup loop, where I write the payload at address $380, then set up some bytes for my 'Fast Nametable Writer', including the payload destination on the PPU Bus (2 bytes) the location of the payload to be copied (usually at $380) the length of the payload, then the value of the PPUAddress after the payload is written (affects the screen scroll)" Phew- that's a lot of controller reads for just one frame, you following along? Luckily if no graphical or audio changes are needed, every frame is only 3 inputs long. (Race-Condition-Prevention, no audio, no graphics)
With this set up, I can essentially make any audio and visual the NES is able to produce (except sample audio), with the huge caveat that the pattern data is restricted to whatever is contained inside the Super Mario Bros. cartridge. Speaking of...
Part 4: Drawing a character not present in SMB1's pattern table
I ask the question "Are you familiar with the travelling salesman problem?". Like most questions, this ends in a question mark. SMB1 does not have a question mark in the pattern data, so I had to get clever. It's actually a series of sprites using the '.' character, assembled in the shape of a question mark. I also used some sprites colored the same colored the background, overlapping others to allow for some thinner parts of the question mark, so it wasn't super blocky.
Part 5: The travelling salesman
The grand finale! (except for the fact that the TAS has a scene afterwards)
I made Mario run to 32 toad houses in a near-optimal path. I intentionally chose the 2nd best path I was able to generate for a joke in the next scene. This was just a combination of all the other tools I had programmed for myself. I have custom music, I update the OAM data to animate Mario running, and whenever he runs over a house I update it's color. The average frame here has 56 inputs.
Part 6: The end
A little card pops up reading "SUCCESS!" Then lists some stats. Mario ran to all 32 toad houses. He ran 1068 pixels, for 617 frames. Toad slides on screen, and says "Thank you mario! But your route was suboptimal!"
And the run ends.
Part 7: Where do I go from here?
Anyway, I'm going to continue to work on this. I don't know what I'll make happen next, other than Toad firing mario from his Travelling Salesman career, and Mario will likely be assigned another role.
I hope this explained it enough! Cheers!
LoganTheTASer
on 1/3/2024 12:49 PM
LOL