Harry Potter and the Anatomy of a Speedrun
Speedrunning is when you try to complete a video game as fast as possible. You can speedrun individual levels, you can speedrun specific categories like 100% where you try and collect all items and complete all quests, etc. but the main category is Any% — get to the end as fast as possible, anything goes. For many games an Any% speedrun looks similar to someone playing the game normally, albeit with a lot of skill 1 Celeste is a great example of this, where even though the 24m speedrun is full of near-perfect inputs, it's understandable to a casual player. . But for some games the Any% run looks completely different, like Super Mario World's current 41-second World Record 2 Who wants to spend the time beating all 7 worlds when you can instead just go into the first level, throw some shells, collect a coin, and warp directly to the end credits? If you want to know why this dark magic works this video does a mostly followable job. . How do people come up with these? Using Harry Potter and the Philosopher's Stone for the GameBoy Advance, I'm going to do a technical deep dive into the Any% speedrun and how you could discover it starting from scratch.
Context and the Speedrun
Harry Potter 1 for the GameBoy Advance (GBA) was released in November 2001 to sync up with the release of the first movie. It was the first Harry Potter video game, and was simultaneously released for PC, Mac, Playstation, and Game Boy Color.
In this case we're looking at the GBA version because there's something unique about the speedrun: while the Any% speedruns for PC and PlayStation take over an hour, GBA can be beaten in under 2 minutes. Here's the current world record, which takes only 1m53s.
The speedrun starts normally 3 Okay, mostly normally. The whole game is in Norwegian because it saves 4 frames of text boxes. . 0:03 When the game first boots up you go through some quick cutscenes covering the beginning of the story up through Harry getting sorted into Gryffindor. 0:17 The actual gameplay starts with you being dropped in Hogwarts and told to get to your Defense Against the Dark Arts class. 0:57 There you quickly learn the first spell of the game, Flipendo, and 1:23 get put into a room to test it out, with Quirrell tasking you with collecting 6 Golden Stars.
Here, things go off the rails. 1:26 Rather than collecting any stars the speedrun uses Flipendo to knock a barrel up, walks around it, Flipendo's the barrel back down, 1:42 and then casts twice at it to phase through the wall. 1:45 Once outside the map Harry walks down to the locked door, prepares to cast at it, and immediately teleports to the closing cutscenes explaining that Nicolas Flamel's stone was destroyed: roll credits.
Now we can see what we're working towards. But how would you ever figure this out if you were the first person to play the game? Big bugs like these (referred to as "sequence breaks") are often composed of smaller bugs pieced together, so like any large project we'll break it down. By building up a number of exploits we can combine them to make the final run.
Defining the End
What defines beating the game? Let's take a look at a more normal (but still incredibly fast) run that beats the game in 1h18m. The final minute shows that after you've used the Mirror of Erised to reflect Voldemort's spells back against him enough times he walks into you, covers the screen in a green box (bug) 4 I'm not sure if this happens on a real device. It may also be due to some non-warp shenanigans that happen in the run. and puts you into a cutscene in the Infirmary where Dumbledore explains what happened. After the end-of-year feast the house cup is awarded, and the credits roll.
In the case of HP1, we'll define the end as getting to the credits roll. 5 This is pretty common for Any% but communities can argue about it. For HP1 this is agreed upon in the speedrun rules: "Timing ends the frame the final text box disappears before the credits." So what makes the credits roll? One good thought is noticing that the ending cutscenes (where there's static images and text boxes you're clicking through) looks very similar to the starting cutscenes explaining how Harry got to Hogwarts. Let's take a closer look. 6 We're done looking at the WR runs so we're back to English screenshots thankfully.
Starting cutscene Ending cutscene
Memory Inspection
It's time to introduce the star of the show, BizHawk 7 The real star of the show is Ghidra but it'll unfortunately have to wait for a future post. . BizHawk is an emulator, which allows your computer to behave like a GameBoy Advance. While the final speedrun can be done on a normal game cartridge, an emulator comes with debugging tools that allow you to inspect and modifying the program's memory, load and save states, and run arbitrary code. The main reason for using BizHawk specifically rather than other emulators like mGBA or NO$GBA is because the speedrun rules for HP1 require it. However it does have other benefits such as a good debugger and support for Lua, a higher-level language that allows us to do avoid having to write our own Assembly code. Once BizHawk is downloaded we can open up an HP1 ROM and start digging in.
As a reminder, our hypothesis is that the cutscenes are similar. Knowing where the cutscenes are stored in memory, and how they're triggered could therefore be very useful. There are three main categories of memory
8
This wiki has some useful breakdowns around the other categories of GBA memory.
that we'll be messing around with. The most important category for us is IWRAM, which is fast and used for most of the current game state (where Harry is in the level, health, characters onscreen, etc). There's EWRAM, which is also used for current game state and has more space, but is slower to access. Together they make up the modifiable memory (collectively referred to as WRAM). And finally there's ROM, which holds the static game code (the logic of what level goes where, how casting a spell works, etc). Each has a memory offset (listed in hexadecimal
9
For normal decimal numbers, 10 is after 9. For hexadecimal numbers A is after 9, and 10 is after F. Hex numbers are customarily prefixed with 0x
, which I use in this post. The 0
is inherited from the octal prefixes in C's predecessor B, but while "x" is in "hex", no one seems to have a good answer for why it's not "h". My favorite answer is because "x" sounds like "hex", but you'd probably have to do a séance with Dennis Ritchie to know for sure.
) for where that data is stored.
- IWRAM -
0x03000000 - 0x03007FFF
- 32KB of data on the same chip as the CPU, and used for time-critical data. Has a 32-bit bus, meaning you can read/write memory of up to 32 bits at once. 10 While all data is stored in bits, most data storage references bytes, which are composed of 8 bits. For 32 bits, that means 4 bytes, and the 32KB is 32 kilobytes. - EWRAM -
0x02000000 - 0x0203FFFF
- 256KB of data not on the same chip as the CPU, and therefore up to 6 times slower. Has a 16-bit bus, meaning you can only read/write memory of up to 16 bits at once, and is therefore slower for 32-bit memory usage. - ROM -
0x08000000 - 0x????????
- Up to 32MB of the actual game code.
We'll start with the assumption that there's something to do with the cutscene in WRAM. Opening up BizHawk with the game booted to the main menu we can select
We'll leave the Size and Display settings in the bottom right as the defaults of "4 Byte" and "Unsigned". With all of that set up we'll click "New" in the top bar, revealing that there are 73,728 addresses 11 IWRAM is 32KB, or 32 * 1024 = 32,768 bytes. Looking for 4 Byte values gives us 32,768 / 4 = 8,192 addresses. EWRAM is 256KB, or 256 * 1024 / 4 = 65,536 addresses. Add them up and you get 73,728 addresses. that could be relevant. We'll use "Previous Value" and "Equal To" as the defaults and click the "Search" button in the bottom right. We haven't changed cutscenes, so this says "the value that we're looking for should still be the same". This filters down the number of matching memory locations, in this case removing 1,319 addresses and bringing us down to 72,409.
Now we'll switch gears. Click through the text boxes until you get to the next cutscene of Hagrid on the motorbike. We'll change the Comparison Operator to "Not Equal To" and click "Search" again, this time translating to "the value that we're looking for should be different". This removes 71,071 addresses, with only 12 remaining. Doing this back and forth with "Equal To" within the same cutscene and "Not Equal To" after changing cutscenes eventually gets us down to one memory location: 0x046D8C
, with a value of 8. It's prefixed with 0x04
so it lives in IWRAM.
12
IWRAM follows EWRAM in the memory, with EWRAM ending at 0x03FFFF
. This makes the next address (the first IWRAM address) 0x040000
, hence the 0x04
prefix.
Restarting from the first cutscene and monitoring that value shows that Privet Drive seems to correspond to a value of 2 and each subsequent screen is one higher, until it hits 9 and you're able to control Harry in Hogwarts. Now let's see what happens if we change this!
Memory Modification
Emulators for older games like those on the GameBoy Advance make restoring and modifying memory trivial. In this case what we'll do is reboot the game, select "New Game", and click through the first textbox until it says "would soon be happening". At this point the next button press will take us to the Hagrid cutscene. If you click
Now let's open up the Hex Editor with 6D8C
, the memory location that we found in the previous step. We can see the value is currently 2. This view has the prefix off to the side and the offsets up at the top, so for 0x6D8C
we go down to row 6D80
and over to column C
.
We'll change this number to 3 by right-clicking it, choosing "Poke Address" and changing the value to 00000003
.
Now go back to the game and advance the cutscene and success! We skip the Hagrid cutscene entirely and go straight to Harry on the doorstep. That's a good proof of concept, but we're trying to get to the credits, so let's see if we can find the value of the cutscene we want.
Using Lua
You could play through the entire game taking note of all of the memory values for the 0x6D8C
address as you go, but that takes a long time. Instead let's do it with code, by writing a script that lets us control what the cutscene address 0x6D8C
is set to, and go through them until we find the credits. Enter Lua!
13
Named after its predecessor, Simple Object Language, which was designed in the '90s to help a Brazilian oil company PETROBRAS manage simulator inputs. Eventually it garnered enough modifications to become its own thing: SOL (sun in Portuguese) becoming Lua (moon in Portuguese) (though it still manages simulator inputs 😁).
We'll create a new script cutscene_iterate.lua
with an initial loop that just goes through frames:
while true do
emu.frameadvance();
end
We want to make it so that when we press a button (say N) we change the value at 0x6D8C
. We'll keep track of what we're setting it with in a value
variable, and when it's pressed we'll load Save State 1 (which we defined as right before the cutscene advances), set 0x6D8C
14
You'll notice that I'm actually setting the value 0x03006D8C
. We need the absolute address so we combine 0x6D8C
with the 0x03000000
offset from the IWRAM description higher up.
to the current value
, and increment value
for the next press. This way if you press N to set the cutscene and A to advance to that cutscene we can see all of the cutscenes in order. Note that we need to keep emu.frameadvance();
15
This wiki details the full list of supported functions by BizHawk.
or the game stops processing.
-- Starting value for the cutscene address
local value = 0;
while true do
if input.get()['N'] then
-- Load the save state right before advancing cutscenes
savestate.loadslot(1)
-- Set the current memory value for the next transition state
memory.write_u32_le(0x03006D8C, value);
-- Next time we click, use the next cutscene
value = value + 1;
end
emu.frameadvance();
end
This actually has a small problem: when you press the N button (unless you're really fast) you'll press it for more than 1 frame. This will continuously increment value
and skip multiple cutscenes. To fix this we'll add in some debouncing logic. When N is pressed down the first time we'll run the code, but set a debounce
flag to true
. As long as it's true
we will skip re-running the logic. debounce
will only be set back to false
after the N key is released, so each press only runs the code once.
-- For making sure that the N press doesn't trigger it on each frame
local debounce = false;
-- Starting value for the cutscene address
local value = 0;
while true do
if input.get()['N'] then
if not debounce then
-- Load the save state right before advancing cutscenes
savestate.loadslot(1)
-- Set the current memory value for the next transition state
memory.write_u32_le(0x03006D8C, value);
-- Next time we click, use the next cutscene
value = value + 1;
debounce = true;
end
else
debounce = false;
end
emu.frameadvance();
end
We can load this final .lua
file with
Once we get into the normal beginning cutscenes we can notice something unexpected. When we set the value with N and advance, the value ticks up by one before showing the next cutscene. Therefore we don't ever directly trigger the Privet Drive trigger, we just see what's next. This means we're not actually seeing the cutscene referred to by that number, we're seeing what follows that cutscene (be it another cutscene, or the location you would be after the cutscene played). Values 2-8 play like the normal beginning of the game, but with 9 it all changes. Instead you find yourself advancing to value 0x0a
, showing a Harry Potter Chocolate Frog card.
17
This doesn't exist in the books, though Rowling did say the main trio eventually got cards. In the game Ron gives him this "brand new Chocolate Frog Card" at the feast after the Stone is destroyed.
Advancing through the cutscenes shows a jump from 0x0a
to 0x0e
with Dumbledore awarding the house cup (a different first cutscene than him talking to Harry in the Infirmary that we saw in the normal ending), then to 0x11
for Slytherin winning the house cup
18
You get a 60 point boost at the end, but Gryffindor can still lose if there's a big enough gap. There's no way in the code for Ravenclaw or Hufflepuff to win, unfortunately for them.
, and finally 0x12
, which is where the credits roll. A reminder that all of these values are in hexadecimal, so 0x12
corresponds to cutscene 18.
Cutscene 0x0e Cutscene 0x11 Cutscene 0x12
With memory editing we can now trivially beat the game, and the Lua Console allows us to easily edit memory by running arbitrary one-line Lua commands in the bottom right textbox. So we start the game, get to the end of the first cutscene, type memory.write_u32_le(0x03006D8C, 17)
(1 before the cutscene we want, 0x12
or 18
, because of the advancing logic), click Enter, advance to the next cutscene with A, and we immediately warp to the credits!
Triggering a Cutscene
This is all well and good, but we're not going to be able to freely manipulate the memory in a real speedrun. So how can we trigger this in the game? Here we'll again go back to the normal playthough for inspiration, and try to find places that trigger a cutscene. The first fullscreen cutscene that you hit is 3m3s in, when finishing the Flipendo challenge room after collecting all 6 stars. This unlocks the initial door, and walking through it displays this accomplishment screen. 19 They're all puns.
Flipendo Exit Door Accomplishment Cutscene
Before going through the door we can check 0x6D8C
and see that it's 0x17
, or 23. Setting it to 0x12
(the credits value) and going through the door successfully jumps to the credits! Interestingly though this jump doesn't have the off-by-one issue that our previous memory exploit had, so we see the Slytherin cutscene before the credits. We now have line of sight to our goal, which is completing the Flipendo challenge, setting the 0x6D8C
address to 0x12
, and then exiting into the credits.
Felix Felicis
Writing a specific value is tricky, but we can also rely on a large amount of luck. From the previous section we know that the value doesn't have to be exactly 0x12
, as 0x11
or 0x0e
or 0x0a
or even 9
will allow you to advance through cutscene slides and eventually get to the credits. 9
is the key here, because it's serendipitously the value that the game leaves you on when you finish the opening cutscenes and first get control of Harry! So if we could somehow walk through the Flipendo door without ever changing the value of the cutscene address we'd be set.
Here's where the second bit of luck comes in. If we monitor 0x6D8C
while doing the challenge, it stays initialized at 9
until we collect the sixth star.
This makes it even easier. Instead of writing a specific value all we have to do is either collect the final star without setting this value, or walk through the door before collecting the final star.
Getting Out of Bounds
While there may be ways to prevent the final star from setting the value it's certainly a lot faster to not collect all the stars, and just find a way through the door—after all, a staple of video game speedruns is getting out of bounds. Here's where playing the game a lot might come in handy: you can just walk through one of the walls. After collecting the 4th star there's a gap in the bottom left of the room with no collision, and you can freely walk through the wall. 20 Figuring this out on your own would definitely be tricky. You could write a script to read and display collision data to make it easier, but the next section shows a more discoverable way out of bounds. Unfortunately this doesn't get us all the way there, because while you can walk near the opening door, the collision prevents you from walking into the door.
Except we can do more than just walk into it! Now that we're in the Flipendo room we have two ways to move Harry's position: walking, and casting a spell.
Walking animation Casting animation
This casting animation is the final piece that we need. At the most outstretched frame of the animation Harry's wand goes past the wall and onto the other side.
If we go down to the side of the door and try to cast Flipendo, Harry reaches out past the collision barrier, intersects the door object, and performs real magic: a credits warp. 21 You can also trigger this by casting facing down (Harry's arm splays out and triggers the cutscene too) but the side angle is a lot clearer as to what's happening.
Getting Out of Bounds Faster
We're most of the way to the current speedrun. The main time spend right now is having to go through the level far enough to access the broken wall, and the time spent walking through out of bounds back to the start. What if we could get out of bounds even sooner? Here's where Harry's lack of allegiance to the XY grid comes in handy. Normally you can't cast through a wall, but if you align yourself as close to a wall as possible using a corner you can get Harry slightly in the wall, letting you cast to the other side.
You can also see this slightly buggy interaction with the wall and barrel. If you push the barrel upwards and walk into it you can actually phase into the wall and squeeze by.
We can combine all three movements (partial wall intersection, barrel collision, and casting animation) by bringing the barrel down towards the door, and phasing through the initial wall. With this we have everything we need to beat Harry Potter and the Sorcerer's Stone for GBA in just two minutes.
Join the Leaderboard!
The speedrun is pretty easy to do (here's my 2m02s time) and more people should give it a shot. The current WR holder Flo203 has created a number of accessible tutorials covering setting up the emulator, how to run Any%, and optimizations if you're looking to improve your time. There's also a great community of people over on Discord if you run into any problems. With only 61 runs on the leaderboard as of now you can be in the top 100 fastest people to ever beat the game just by submitting!
Credits
I wouldn't have been able to create this without a lot of great resources. I found out about the run from this short, and wouldn't have dug into the technical side of it if it weren't for this Ghidra writeup around GBA reverse engineering (more posts to come). While this post details how you could have found something like this on your own, in practice a huge thanks is owed to JaGoTu discovering the Flipendo OOB back in Jan 2021, StarrlightSims for discovering the cast clip and getting the first real run with it, and countless others for optimizing it down to the current WR.
-
Celeste is a great example of this, where even though the 24m speedrun is full of near-perfect inputs, it's understandable to a casual player. ↩︎
-
Who wants to spend the time beating all 7 worlds when you can instead just go into the first level, throw some shells, collect a coin, and warp directly to the end credits? If you want to know why this dark magic works this video does a mostly followable job. ↩︎
-
Okay, mostly normally. The whole game is in Norwegian because it saves 4 frames of text boxes. ↩︎
-
I'm not sure if this happens on a real device. It may also be due to some non-warp shenanigans that happen in the run. ↩︎
-
This is pretty common for Any% but communities can argue about it. For HP1 this is agreed upon in the speedrun rules: "Timing ends the frame the final text box disappears before the credits." ↩︎
-
We're done looking at the WR runs so we're back to English screenshots thankfully. ↩︎
-
The real star of the show is Ghidra but it'll unfortunately have to wait for a future post. ↩︎
-
This wiki has some useful breakdowns around the other categories of GBA memory. ↩︎
-
For normal decimal numbers, 10 is after 9. For hexadecimal numbers A is after 9, and 10 is after F. Hex numbers are customarily prefixed with
0x
, which I use in this post. The0
is inherited from the octal prefixes in C's predecessor B, but while "x" is in "hex", no one seems to have a good answer for why it's not "h". My favorite answer is because "x" sounds like "hex", but you'd probably have to do a séance with Dennis Ritchie to know for sure. ↩︎ -
While all data is stored in bits, most data storage references bytes, which are composed of 8 bits. For 32 bits, that means 4 bytes, and the 32KB is 32 kilobytes. ↩︎
-
IWRAM is 32KB, or 32 * 1024 = 32,768 bytes. Looking for 4 Byte values gives us 32,768 / 4 = 8,192 addresses. EWRAM is 256KB, or 256 * 1024 / 4 = 65,536 addresses. Add them up and you get 73,728 addresses. ↩︎
-
IWRAM follows EWRAM in the memory, with EWRAM ending at
0x03FFFF
. This makes the next address (the first IWRAM address)0x040000
, hence the0x04
prefix. ↩︎ -
Named after its predecessor, Simple Object Language, which was designed in the '90s to help a Brazilian oil company PETROBRAS manage simulator inputs. Eventually it garnered enough modifications to become its own thing: SOL (sun in Portuguese) becoming Lua (moon in Portuguese) (though it still manages simulator inputs 😁). ↩︎
-
You'll notice that I'm actually setting the value
0x03006D8C
. We need the absolute address so we combine0x6D8C
with the0x03000000
offset from the IWRAM description higher up. ↩︎ -
This wiki details the full list of supported functions by BizHawk. ↩︎
-
This is in the front grounds outside Hogwarts, where you end up after a Quidditch game. ↩︎
-
This doesn't exist in the books, though Rowling did say the main trio eventually got cards. In the game Ron gives him this "brand new Chocolate Frog Card" at the feast after the Stone is destroyed. ↩︎
-
You get a 60 point boost at the end, but Gryffindor can still lose if there's a big enough gap. There's no way in the code for Ravenclaw or Hufflepuff to win, unfortunately for them. ↩︎
-
Figuring this out on your own would definitely be tricky. You could write a script to read and display collision data to make it easier, but the next section shows a more discoverable way out of bounds. ↩︎
-
You can also trigger this by casting facing down (Harry's arm splays out and triggers the cutscene too) but the side angle is a lot clearer as to what's happening. ↩︎