Paper Mario: TTYD Credits Warp - Explained
Hey everyone, Malleo here. For about 5 years, a small group in the Paper Mario 64 community has been working to achieve a state called Arbitrary Code Execution or ACE. ACE is notorious in the speedrunning community, as it has the potential to allow the player to take total control over the game’s code and reprogram it. A great example is Masterjun’s Super Mario World ACE demonstration, in which code for Snake and Pong is written and executed within Super Mario World. As you can imagine, one of the most well-known applications for ACE in speedruns is the potential to perform a credits warp. In this scenario, you find a way to change the flow of execution in the game’s code to load the credits sequence.
A credits warp is the holy grail in any speedrun, as it is almost always the last major timeskip that will ever be found in a game. Rain, Fray and MrCheeze, with the help of several others in the community, finally accomplished Arbitrary Code Execution and showcased it by writing code to warp to the credits, drawing their names on-screen, and changing the background music. Huge congratulations to the Paper Mario 64 team on this amazing discovery. I’ll have links regarding their work in the description. Two days after the discovery of Paper Mario 64’s credits warp, I went live on Twitch to showcase something that SolidifiedGaming and I had just completed. This is what happened on stream.
Just a day after the Paper Mario 64 Credits Warp discovery, SolidifiedGaming and I managed to exploit a buffer overflow and warp to the credits in TTYD a mere 25 minutes into the game. This video will explain the theory and planning behind how this came to be, and discuss what this means for the future of TTYD speedrunning as we know it. Hope you enjoy! To begin our story, let’s first talk about events, or EVTs for short. Each event represents a set of commands and stores information about the current state of the event execution. Examples of events include everything from setting up objects and NPCs upon loading a room to orchestrating cutscenes. When the game runs an event, the commands specified in the event are executed based off of a scripting language defined within the game’s code.
Therefore, we can think of events as a running instance of script code. To manage the order in which events should be run on a particular frame, the game uses a queueing system, through which it starts running events from the start of the queue and works its way towards the end of the queue. The game has room in memory to store at most 256 events on any given frame. This may sound like plenty, until glitchhunters get involved. We knew for a while you could pause and hammer at the same time, resulting in Mario continuously hammering while in the pause menu. As you could guess, we call this pause hammering, and we’ll see this technique come up a couple times later in the video.
While in the pause menu, hammer events are added to the queue, and since we’re paused, the events are not yet executed. Thus, they remain in the queue every frame until shortly after unpausing. Pause-hammering long enough allows you to fill up all 256 slots of the queue.
Afterwards, any subsequent events are not added to the queue since it is now full. In mid-2020, SolidifiedGaming found that certain events, such as opening doors, spawn additional events in such a way that the additional events are added to the queue even if the tables are full. SG played around with this mechanic for a while until one day, her game crashed.
This had never happened before, even after months of experimentation. Thrown off by this, SG began investigating. As it turns out, there was an invalid memory read. While trying to execute events in the queue, the game tried to access a region of memory that isn’t actually accessible. What does that mean? As SG soon learned, the game started reading from memory located *way* past the normal event data region and was instead trying to reach data that occurred much later in memory. Since it eventually reached the end of the main valid memory region, the game crashed.
But what if it hadn’t? Then the game would have started reading data at this location as if it was event data… It was at this exact moment that SolidifiedGaming realized the significance of what she had just uncovered and knew that if there were ever a way to pull off a credits warp, it would be through this method. If we could control where the game started reading event data from, and manipulate that data in a particular way, then the game could interpret that data as whatever script command we wanted. The game uses events to load a new map or area of the game, so that means we could run script code to change the map to the credits sequence. From that point onward, SG sought to reverse engineer the inner-workings of events even further in order to explore how we could manipulate and run event data to jump to the credits. Let’s start by discussing all the relevant information that is required to understand the steps necessary to jump to the credits sequence.
First, let’s discuss the two tables involved in the event queue. The first table is an array of unique identifiers for each event that the game wants to queue up. Let’s call this table the “ID table”. The second table is an array of indices that are used to determine where in memory the data for a given event actually exists.
Let’s call this the “Index Table”. There is a one-to-one mapping between an ID and an index. Strictly speaking, the ID in the first slot corresponds to the index in the first slot, the ID in the second slot corresponds to the index in the second slot, and so on. As I mentioned earlier, both tables can hold up to 256 elements. Second, the event or EVT is the actual data structure that contains information about the commands that should run and information about the current state of the event’s execution.
For simplicity’s sake, I’ll just show the fields that are relevant for the purposes of this video. There is a field located 8 bytes into the structure that we’ll call “Flags”. It’s a bit field, meaning the game may read particular bits at that address to determine information about the current state of the event’s execution. In order for a queued event to run, the first, fourth, and seventh bit must be set to 0, and the eighth bit must be set to 1.
The other bits can be either 1 or 0. Based on this rule, the flags byte must be one of the following values [on-screen]. At offset 0xA is a field we’ll call “opcode”.
Short for operation code, the opcode is always the first component of a script command and just specifies what kind of operation to perform. The opcode refers to a set of operations that the game’s built-in scripting language can perform. Examples of opcodes include an add, which just takes two numbers and sums then, and user_func, which allows the game to call one of many functions that exist within the game’s code. At offset 0x14 is what we’ll call “NextCmdPtr”.
This field acts as a pointer to the next command that this event will execute. Further into the structure at offset 0x15C, we have a unique identifier for this event. As I mentioned earlier, an ID is placed in the ID Table when an event is queued. However, the game also stores that same ID in the event itself. If the ID in the ID table does not match the ID in the event itself, then the event does not run.
This check is likely implemented to avoid certain problems with running and terminating events in the queue. Lastly, there are two floating point numbers we’ll call “Timescale” and “TimeScheduledToRun” located at offsets 0x164 and 0x168 respectively. Skipping over the details, as long as Timescale and TimeScheduledToRun sum to 1 or greater, then the event can execute.
Just to reiterate, the game checks for valid flags, ID, timeScale, and TimeScheduledToRun. Only when all of those checks pass does a queued event run. Now let’s walk through how the game actually queues events. To start, given the fact that we need to be able to uniquely identify events, the game has a counter which tracks the next ID to be assigned. Let’s just call this the “Next Event ID”. This counter is incremented every time an event is queued.
This way, there is no fear that a given ID will accidentally refer to multiple events. If the game wants to queue up an event, it first finds where in the queue it can place it, takes “Next Event ID”, adds it to the ID table, and increments “Next Event ID”. Then the game determines where in memory it can store the new event, and stores an index in the index table that corresponds with this memory location. Event indices represent what position in memory an event is located at. To find where the event is stored, you first take the index and multiply by 0x1B0, which is the size of one event.
It then adds this result with the start of the event memory region. The result is the location of the desired event. The game will then read the event information to determine if it should run and what script code it should execute. So let’s simulate what SolidifiedGaming did by queueing a door, pause-hammering, filling up the event queue, and then un-pausing. When we unpause and the door event generates two additional events, the IDs for the additional events are accidentally written into the first two slots of the index table.
The ID table and the index tables are right next to each other in memory, so when event IDs are written past the end of the ID table, they are instead written to the start of the index table. Now that the door event ran in the first slot of the queue, the game will now try to run the second event in the queue. When the game does so, it will associate the second ID in the ID table with the second index in the index table, which is now an incredibly high value compared to what the indices normally are. The game will take this really large number, multiply by 0x1B0, and add it to the start of the event memory region.
As a result, it will start reading memory located way past the normal event memory region. The game is now reading data from other data structures as if it’s event data. For example, it’s possible that the game starts reading event data from Mario’s data structure and interprets Mario’s coordinates as the event flags, opcode, or nextCmdPtr. Now that we know that it is possible for the game to read event data from unintended memory locations, the question is: What exactly can we do with that knowledge? Through reverse-engineering, we know that to change to a different map, the game calls the mapchange function and specifies the desired room to load. One such map is the credits sequence. If we can cause the game to read event data that is interpreted as a mapchange function call to the credits sequence, then we would be able to warp to the credits.
With all of that out of the way, let’s talk about the route that we began looking into. Shortly after discussion about a potential credits warp began, PistonMiner was quick to suggest using something called the EFF heap to manipulate data for this scenario. Effects, or EFFs for short, represent what are essentially particle effects. This includes dust from walking around, the exclamation mark that appears over enemy’s heads, and grass flying up in the air when walking around the grass patches in Petal Meadows. Though the abbreviation is similar, they are not at all associated with events besides the fact that some events do actually generate effects. The effect heap allows the game to allocate space for the effect data and free up that memory when the effect is despawned so other effects can use that space later.
If multiple effects are loaded at the same time, then more of the heap will be filled with data. If we overflow the ID table such that a particular ID is written to the index table, the game can begin reading event data from the effect heap. We quickly realized that this could be a viable route; we have control over what effects are loaded and in what order, and we can also slightly vary where in the heap the game reads event data from. Should we manage to find some combination of effects such that we have control over what data is written to the fake event data structure, then it is entirely possible that we could call the mapchange function. To ultimately call the mapchange function after running fake event data, SolidifiedGaming determined the best course of action would be to set the opcode to 0x0, and set nextCmdPtr to some predetermined memory address.
Opcode 0x0 simply fetches the next command, which is located where nextCmdPtr points to. The plan was to set nextCmdPtr to some manipulable memory region that can be interpreted as a mapchange function call. So, how exactly could we go about pulling all of this off? We set out to find a region of the heap that we could rely upon to write event data. Let’s cover how the heap works so we can understand why caution needs to be taken.
When the game needs to place effect data in this heap, it searches the heap from start to end until it finds the first free slot to allocate data to. If the data in a slot is no longer needed, then the slot is freed up and the game can use that slot to allocate new data again. However, the data previously written in that slot persists as long as a new effect doesn’t overwrite it. The main issue we wanted to avoid was writing event data in a slot too close to the start of the heap. If we read data at the very start of the effect heap, then that data would be more susceptible to changing, every time a single effect is loaded into memory.
We want our data to persist until we have an opportunity to execute the event, so we don’t want any of our data to be overwritten. If we use a portion of the heap further away from the start, then it will take quite a few loaded effects to overwrite our data. We found a location far enough into the heap such that we can manipulate it in a sufficient way and not worry about the data being accidentally overwritten.
With the memory location decided, let’s discuss how we began to write the necessary data for the fake event data structure. I apologize in advance if the order of explanations is confusing, as certain pieces of this puzzle may not be explained until slightly later. However, I think the best way to go about this is to explain this warp chronologically. To start, we buy a Mushroom and soft-reset the game.
We reset the game in order to reset “Next Event ID” to 1 and I’ll explain why a bit later. When we load back in-game, we now care about setting up our event data structure. We want to set flags, opcode, ID, timeScale, and timeScheduledToRun all to values that will pass the necessary checks and set us up to fetch instructions from a manipulable memory region. To start, we pause hammer and use a Mushroom while doing so. This results in Mario continuously hammering, which generates effects, while the Mushroom is consumed, which also generates effects. If done at the right time, this, in combination with effects generated by butterflies floating around Petalburg, results in flags being set to 0x41 and opcode being set to 0xC1.
Since 0x41 is one of the valid flag values that I mentioned, the flags check passes. However, this opcode doesn’t match the fetch opcode I mentioned earlier. As it turns out, in the event an invalid opcode is provided, the game basically treats this the same way as a fetch, so it will still look at nextCmdPtr to retrieve the next script command.
Thus, this opcode is satisfactory for our situation. With those fields satisfied, we now make our way left. We stop moving every so often to despawn effects.
If we generate too many effects, we may end up overwriting flags and opcode in the heap. However, we also need to generate enough dust effects to write a suitable ID to our fake event data structure while leaving flags and the opcode intact. Coincidentally, loading dust effects in combination with the butterfly effects in a certain order writes data to the heap such that ID is written, but flags and opcode are not modified. Once we leave the room, ID is set to 0xFF, timeScale is set to 1 and timeScheduledToRun is set to 76.727.
Because the sum of timeScale and timeScheduledToRun is greater than 1, this check is satisfied. Recall how the ID in an event needs to match the corresponding ID in the ID table. If the two IDs don’t match, then the game knows something odd has occurred and it prevents the event from running. For now it’s a bit unclear why exactly we set the fake event’s ID to 0xFF, but we’ll get to that in a bit.
Let’s review. The flags field is set properly, opcode is set properly, ID is set properly, even if I haven’t explained why yet, and timeScale and timeScheduledToRun are set properly. What’s left? NextCmdPtr, which quickly became the most difficult component of the credits warp setup. Just like we’ve been doing with the other fake event fields, we now need to spawn particle effects in some order such that a consistently manipulable value is written to nextCmdPtr in the fake structure. Not only that, but we had to make sure that the particular combination of effects, when written to the heap, does not overwrite the flags, opcode, ID, timeScale, or timeScheduledToRun.
It was quickly realized that finding such a viable combination would be a tough task. This took several weeks where we mostly documented effect data structures and tried writing programs to simulate the heap. After weeks of effort, we weren’t successful in finding a viable combination… until SolidifiedGaming had a stroke of luck. While messing around in-game, she somehow wrote Mario’s position to the nextCmdPtr field by loading some precise combination of effects into memory. After analyzing the heap, SG was able to determine what combination of effects were used to write Mario’s position to nextCmdPtr. We now have a replicable method by which we can control what’s written to nextCmdPtr by precisely setting Mario along the X-axis when spawning this particular combination of effects.
Maybe we could use Mario’s X-position to write a valid memory pointer to nextCmdPtr? Now the question became: What memory location could we take advantage of, so that we have sufficient control over enough bytes of data to write script code to call the mapchange function with the credits sequence? SolidifiedGaming found a viable memory location in which GameCube controller inputs are stored. At this particular location, starting at memory address 0x8040F0D0, controller data is stored in the following way: Byte 1 is a bitfield, where the left 3 bits are 0, followed by Start, Y,X,B, and A where a bit is set to 1 if the button is being pressed. Byte 2 is also a bitfield, where the first bit is 1, followed by L, R, Z, D-Up, D-Down, D-Right, and D-Left. Byte 3 is the X component of the Analog Stick, which is a value in the range of 0-255. Byte 4 is the Y component of the Analog Stick, also in a range of 0-255.
Byte 5 is the X component of the C-Stick with range 0-255. Byte 6 is the Y component of the C-Stick with range 0-255. Byte 7 is the L-Shoulder Analog input, where a value between 0-255 represents how hard you are pressing down the shoulder button, and Byte 8 is, similarly, the R-Shoulder Analog input. These 8 bytes repeat 4 times for each of the 4 controller ports. It’s important to mention the following constraints, as a result of the layouts of the bytes mentioned: For the first bitfield, it can only have values between 0x00 and 0x1F due to the limited number of buttons that are encoded in that bitfield, and for the second bitfield, if you aren’t holding any buttons the byte is, for some reason, 0x80.
That is to say, because the top bit is 1, we can’t produce any value lower than 0x80. Because of these constraints, we need to determine where in the controller data we want to start reading bytes from. Let’s look at the typical script command the game runs to change maps: “User_func [evt_bero_mapchange] [map_name] [loading_zone_name]” Again, the user_func opcode is used to call the function specified right after, which in this case is evt_bero_mapchange. To change to a specific map, we provide a memory location that contains the desired map name.
Lastly, you can specify a memory pointer that signifies what loading zone to enter the map from. Now let’s look at how this is represented in the game’s code. “0000005B 80079FE4 805B4C7C 00000000”. Let’s look at the first 4 bytes. The fourth byte in this data is the opcode. 0x5B is used to represent user_func.
Next, 0x80079FE4 is the location of the mapchange function. After that, 0x805B4C7C is the location of the map name, in this case “end_00” and that’s followed by all zeroes. When the game doesn’t need to specify a loading zone to enter the map from, like in the case of the credits, the field is left as zero.
If we were to encode this data with controller inputs starting at Controller 2, then we would have enough bytes to fit this command. However, to simplify things a bit more, it actually turns out that the loading zone field doesn’t actually matter at all. Even if it’s an invalid memory pointer, the game is still able to run the mapchange command.
So now we’re left caring only about the first 12 bytes. One last thing to mention is that if there are two memory locations that contain “end_00”, then you can use either one and the command will still function the same way. For no particular reason, we chose to use memory address 0x802ECC84 instead of the intended one. Now with a simplified and concrete command, let’s encode it! We don’t really want to read from Controller 1’s data since we’re actively using that to play the game. To make things go a bit smoother, let’s try starting from Controller 2’s data. Controller 2 will be used to write the opcode and the mapchange function pointer.
But now we have a problem. We need to specify the memory location of the map_name. However, the first byte of Controller 3 cannot have a value larger than 0x1F. Thus we are unable to write a valid memory pointer. Instead, let’s try reading starting from Controller 2 byte 2. Then we end up using Controller 3 byte 1 to encode the last byte of the mapchange function pointer.
We need to encode 0xE4, but we can’t have values larger than 0x1F in that first byte, so let’s try starting from Controller 2 byte 3. We face the same issue. We need to encode 0x9F but we can’t use the first byte of Controller 3 to do so. Let’s try to start at Controller 2 byte 4. And… It works. If we start at 0x8040F0DB, which is Port 2’s analog stick Y position, then we are able to encode the mapchange function call along with the pointer to the “end_00” string to specify the credits sequence.
The inputs look like this. So, that’s fantastic! One of the last things we need to do is determine what X position we need to have Mario stand at in order to write 0x8040F0DB to nextCmdPtr. Mario’s coordinates are encoded as floating-point numbers. If we want to encode 0x8040F0DB as a float, this yields a position of -5.96e-39. If you assume that a position change of 1 unit represents a meter, then the distance between our desired position and 0 is smaller than a Planck length. That is such an unfathomably small position that it is not at all realistic to ever achieve this position.
It’s a dead-end. We can’t use Mario’s x position to write the controller data location to nextCmdPtr. In fact, any valid memory pointer between 0x80000000 and 0x817FFFFF, which is the range for the game’s main memory region, is inaccessible for the same reason.
So this must mean that we can’t use Mario’s x position to write any valid memory location to nextCmdPtr. Except, this isn’t the only valid memory range. The range I just provided actually refers to the GameCube’s cached main memory. The cache is essentially related to how memory is transferred between main memory and the CPU.
There is actually an uncached main memory region located between addresses 0xC0000000 and 0xC17FFFFF. The values stored here are usually identical to what is stored in cached main memory, and that means controller data is also stored here. So instead of using 0x8040F0DB, is it possible to write 0xC040F0DB to nextCmdPtr? 0xC040F0DB corresponds with an X-position of -3.01470065117.
Because this is a much larger number, this is entirely reasonable to reach. So let’s try to get to that position. I found two different analog stick arrangements in opposite directions along the X-axis. The left arrangement gives an X speed of -.12486267, and the right arrangement gives an X speed of +.01785278.
Holding the left arrangement for one frame and the right arrangement for 7 frames results in a position offset of -.00010681 units, or 0x1A3. This is a pretty small position offset, so I can get pretty close to the position we need, but not quite. I need to offset my position by less than 0x1A3, so I had to find some other way to change my position. For whatever reason, possibly due to an ever-so-slightly rotated camera angle, holding straight down on the analog stick results in my X position memory value incrementing by exactly 1 and when I hold up it doesn't change. So, I repeatedly moved down to increment my position to 0xC040F0DB. Our next goal is to write this position to memory.
By spawning the precise combination of effects that SG found, we manage to write 0xC040F0DB to nextCmdPtr. So, our fake data structure is set up, and when the game fetches a script command from the controller data, we know what controller inputs are needed to encode the mapchange function call. All that's left is making the game execute this fake event. We head back to the right.
Before reaching Petalburg, we shake the bush to increment “Next Event ID”. Recall that to pass the ID check for our fake event struct, we need to have the ID stored in the ID table match the ID stored in our fake event struct. However, we also somehow need to overflow a very high event ID into the index table in order for the game to start reading from our fake event structure. So the question is, how can we keep 0xFF in the ID table while overflowing a much, much higher ID into the index table? It just so happens that there is a particular event pertaining to the item shop that is spawned upon entering Petalburg and remains in the queue so long as we are in the current room. We entered Petalburg after incrementing the bush a few times such that this persistent event now has an ID of 0xFF in the queue. Even though this event is persistent, loading other events in the room causes the persistent event to be shifted to the second slot in the queue.
Loading subsequent events causes the persistent event to remain in the second slot. Moving forward, when we go to set up the overflow, we need to keep in mind that the 0xFF ID will be in the second slot of the ID table, and thus we need to make sure we have the index for our fake event structure in the second slot of the index table. With the 0xFF ID set up, we enter the Petalburg shop and approach the shop sign. You can press A to open a menu and press B right after to close the menu. The menu generates and queues an event, which causes the “Next Event ID” to be incremented by 1.
The index that corresponds with the region of the effect heap that we used to set up our fake event structure is 17,509. We want to read this sign repeatedly until “Next Event ID” gets close to 17,509. Unfortunately, this takes about 9 minutes when done perfectly. But honestly the memes as a result of this were pretty funny. Once we reach an event index of around 17,000, we can get started filling up the event queue so we can overflow IDs into the index table. One main concern is the game actually refreshes the event queue every frame.
We need to somehow queue up 256 events to fill up the queue without any of them despawning. To do so we can make use of a pause hammer. We used a pause hammer earlier to set our flags and opcode, but that ultimately occurred because the hammer spawns effects whose data was written to the effect heap. Hammers also happen to generate events, and pause hammering continuously queues up hammer events without removing them from the queue. So we can start to fill up the table.
When we start pause hammering, we also activate the door. Doing so buffers the door opening until we unpause. As I mentioned near the start of the video, it’s not possible to overflow the event queue tables unless an event itself generates more events, but doors can be used to overflow the table as they generate additional events. While in the pause menu, hammer events are constantly being queued up. Once the queue is full, we need to start paying attention to “Next Event ID”.
Even though the game does not queue events if the table is full, it still increments “Next Event ID” when trying to do so. Thus, we can sit and wait in the pause menu as “Next Event ID” gets closer and closer to 17,509. Eventually we want to unpause such that, right before the game runs the queued events, ”Next Event ID” is 17,508. If done correctly, then the following order of events occurs. The door event is executed, during which it spawns two additional events.
The first additional event retrieves “Next Event ID”, which is 17,508, and adds that to the ID table in the queue. Because the table is full, the ID is placed into the first slot of the index table. “Next Event ID” is then incremented to 17,509. The second additional event retrieves “Next Event ID”, 17,509, and places that ID right after the first additional event ID, which means it is placed in the second slot of the index table.
The game has finished executing the door event and now looks at the second event in the queue. The ID is 0xFF because of the persistent event that spawned upon entering Petalburg, and the index is 17,509 because we overflowed the second additional event ID into the index table. The game takes 17,509, multiplies it by 0x1B0, and adds it to the start of the event data region to find where it should read event data from.
As a result, it navigates to our fake event structure. It compares the ID of 0xFF from the second slot of the ID table with the ID inside our fake event struct. The check passes.
Then it checks for the flags, which is one of the accepted values, so the check passes. Then it sums timeScale and timeScheduledToRun which is greater than 1. The check passes. All checks have passed, thus the event is executed.
The fetch opcode is read, so the game fetches the script command from the memory address specified by nextCmdPtr. Because we set nextCmdPtr to the address of port 2 controller data starting at the Y component of the analog stick, we can encode the mapchange command and specify the credits map ID. This command is executed and the mapchange is successful. The screen begins to fade out, and we are presented with the credits screen. As a side effect of still having Frankly in our party, he appears for the entirety of the credits behind any images on-screen.
After only 25 minutes, the game has been beaten. Proceeding to save your file after the credits and load it results in Mario, Goombella, and Frankly returning to Rogueport in a post-game state. The game recognizes that you have beaten the Shadow Queen and completed the credits sequence. This leaves us with two main questions, the first being, Is this RTA viable? By no stretch of the imagination is this humanly possible.
There are far too many details that go into this route that could never be replicated by a human, namely the floating point perfect position that’s required. The second question is whether or not a faster route can be found. We see no reason to believe otherwise. In fact, we believe that there will at some point be an RTA viable route for Credits Warp.
It’s just a matter of continuing to document the game’s source code and discover other routes to use. What happens to the future of the TTYD speedrun? Should a human perform a credits warp in this game, the TTYD speedrunning community will split any% into two separate categories, with the credits warp taking the place of any% and a separate category called “any% no arbitrary script execution” being created. As far as speedrunning competition is concerned, very little will change. For the credits warp, it’s also worth keeping in mind that we don’t need to rely on the particle effect heap.
We could always use a different region of the game’s memory, should we find that it is manipulatable in just the right way to satisfy all the game’s checks. Most significantly, that would prevent the need to read the sign for 9 minutes because we wouldn’t need “Next Event ID” to be as large when it overflows into the index table. All of that being said, I expect there to still be a good chunk of timesave once we wander upon a new route.
It’s worth making the distinction that the method through which we wrote script commands is not considered Arbitrary Code Execution. We aren’t actually executing arbitrary code. We’re not actually encoding assembly instructions to reprogram the game. Instead, we’re simply encoding a single script command. Because we’re limited to the bounds of the scripting language, we don’t have total control over the game, yet.
We do expect we could use this method to reach an arbitrary code execution state, but that will require some more planning and research, but it’s something I’d like to help make happen in the future. I’ve seen a lot of people say that they actually lose interest in a speedgame that has a credits warp. I can understand that. The idea that some ridiculous glitch allows you to skip all entertaining and memorable parts of a game just ruins the fun. But… Isn’t it entertaining and memorable in itself, that as a result of community-wide efforts to reverse-engineer the game’s code and mess around with events, a vulnerability was found that could be used to execute any script command at will? Isn’t it memorable to know that there will never be a more significant timesave found for this game? Isn’t it memorable that, since I helped make the first TTYD TAS more than 7 years ago, that we have lowered the speedrun by 4 hours and 27 minutes? To me, this has been one of the most memorable moments in my entire online career.
It has been the most entertaining excursion I have ever gone on in my entire online career. All that being said, for me, this only strengthens my appreciation and interest for Paper Mario: The Thousand-Year Door. I want to give a huge thank you to SolidifiedGaming. SG discovered the crash due to the event queue overflowing, determined why it happened, realized the potential of this exploit, and began routing a Credits Warp. She was monumental in the planning process and knew what we had to do along every step of the way. It was a pleasure to work alongside her and tackle some of the tough aspects of this route, particularly achieving the floating point perfect position.
I’ve always wanted to help discover a credits warp, and it’s because of SG that I had the opportunity to be along for the ride. Thank you to PistonMiner for theorizing that we could use the effect heap for our fake event structure. Additionally, he confirmed for us that Mario’s float position can be interpreted as a memory pointer into uncached main memory. Thank you to JMC4789, and booto for verifying on-console that we can reliably access uncached GameCube memory for this route, as this is something that hasn’t been explored very extensively before.
Thank you to PistonMiner and Zephiles for their development of the TTYD practice codes. They help make speedrunning practice for TTYD a lot easier, and were pretty useful as we experimented with different memory locations to set up our fake event structure in. Thank you to all the friends and community members that looked over and helped edit this video script. Lastly, thank you for watching.
The topic of this video is more complex than what I normally feature in my other videos, but I hope you enjoyed it nonetheless. And with that, have a good one!