- Emulator: lsnes rr2-beta23
- Rom: Final Fantasy VI (J)
- Aims for fastest time
- Manipulates luck
- Executes arbitrary code
- Features mid-frame resets
- Corrupts save data
- Ends input early
We have shown a proof of concept for arbitrary code execution in the Japanese version of the game a year ago. However, it requires to gather one of several items that are only available in the late game (after about 2 hours of run), so we let it go. After the discovery of the game over glitch that lead to the latest TAS, featuring 15 boring minutes of game overs, we were eager to find another route that could beat this time and be more fun to watch. This is where we remembered of the mid-frame reset technique used in a couple of published TAS, that might lead to getting these items much quicker. This was the beginning of our third work on this game. Comments are embedded in the above video and are available here.
Glitch leading to ACE
The glitch used to get control over the game is based on the Japanese exclusive equip glitch. This glitch allows you to equip any item in any equipment slot. To equip an item on a weapon slot, for example, you must:
- get rid of every weapon that a character can equip, so that the equip menu on that slot would show an empty list
- place the item in the last (256th) slot
- select "Optimize"
In unassisted speedruns, this glitch is used to equip items that raise your defense and/or magic defense drastically. In this TAS, we are using this glitch on the weapon slot.
During a fight, when you attack with a weapon, the game loads the weapon graphics properties from address
$ECE400+8*id
into $623B-$6242
. Address $6240
, which stores if the weapon has a short or long range animation, takes values between 0 and 4. According to this value, the game calls a different function, as shown below:
$C1/C217 AD 40 62 LDA $6240 load $6240 $C1/C21A 29 7F AND #$7F $C1/C21C 0A ASL A multiply by 2 $C1/C21D AA TAX $C1/C21E 7C 21 C2 JMP ($C221,x) call one of the routines below Function pointers: $C1/C221 34 C2 call $C1/C234 when $6240 = 0 $C1/C223 47 C2 call $C1/C247 when $6240 = 1 $C1/C225 2B C2 call $C1/C22B when $6240 = 2 $C1/C227 C0 C2 call $C1/C2C0 when $6240 = 3 $C1/C229 21 C3 call $C1/C321 when $6240 = 4
However, for items that are not weapons, address
$6240
can store any value, so that the jump instruction above leads to many wrong addresses. Among all the wrong jumps, the case with address $6240 = 0x07
(weapons X-Ether, Gold Hairpin, Czarina Ring or Charm Bangle) is interesting because the game jumps to address C1/8D7A
which starts with instruction:
$C1/8D7A 1B TCS Push accumulator to the stack pointer
This instructions corrupts the stack, and will lead us to full control of the game (detailed in another section). The goal of the present TAS is then to collect one of the four items to trigger the ending sequence.
Mid-frame reset
As said in the introduction, all of the four items are only found in the later part of the game. This is why we will be using mid-frame reset to corrupt the game saves. Here are the different options that we used in this TAS:
World Map teleport
World Map coordinates are stored in addresses
$1F60
(X) and $1F61
(Y). We can save once at coordinates (X1,Y1), then move to coordinates (X2,Y2), save on the same slot and reset just after the game overwrites the X coordinate. We will be left with a savefile containing (X2,Y1) stored coordinates. This helps to move rather freely around the World Map. Only a few spots are not accessible with this technique (Thamasa and the Triangle Island).
World Map teleport anywhere
This technique can only be used once. The principle is that the game starts inside a town (Narshe) for the first 15 minutes, and the World Map coordinates are never written until you leave the town. They contain uninitialized values until then. When you exit Narshe normally, the game copies the parent coordinates (
$1F6B-$1F6C
) to current coordinates ($1F60-$1F61
). We can manage to load the uninitialized values as coordinates:
- We save before leaving Narshe in slot 1
- We save in the World Map in slot 2
- We load slot 1
- We save on slot 2 and reset just after
$1F61
is overwritten - When loading slot 2, we will be on the World Map at coordinates corresponding to the uninitialized values.
In lsnes emulator, it is possible to manipulate the initial RAM values to some extend by setting the value of the Real Time Clock that acts as a seed. The first value of the RAM is set to the lower byte of the RTC, and each consecutive value is generated using a recursive formula:
value = (value >> 1) ^ ( ( (value & 1) - 1) & 0xedb88320)
Map warping
Map id is stored as a 2-bytes value in address
$1F64
. Using the same principle as with coordinates, it is possible to warp to a new map by using one byte from one map and another byte from a second map. Here is the list of (hopefully) all save points in the World of Balance, and the new interesting maps that we have access using this trick:
Map | Map ID--WoB Save Points-- World Map | 0000 Narshe cave entrance | 0029 Narshe cave | 0032 Narshe class room | 006B Mt. Kolts | 0067 Returners Hideout | 006E Lete River | 0072 Lete River | 0072 Scenario Choice | 0009 Phantom Train - Tail | 0092 Phantom Train - Middle | 0095 Phantom Train - Head | 0099 South Figaro - Duncan | 0054 South Figaro - Basement | 0058 Narshe - Kefka fight | 0016 Magitek Factory | 010E Minecart | 0110 Magitek Factory Exit | 00F0 Cave to the Sealed Gate | 0182 Esper Cave | 0177 Floating Continent - beginning | 018A Floating Continent - cave | 0166--Interesting MapsDaryll's Tomb | 0129 Fanatics Tower | 016B, 0167, 016E, 0172 Caves to the Ancient Castle | 0192 Hidon's Cave | 0195 Kefka's Domain--Pipe Room | 0199 Gogo's Room | 0116
Checksum
The game has a mechanism to detected corrupted savefiles, so we have to bypass it. When saving, it computes the sum of all values of the savefile in a 2-bytes value (
$1FFE-$1FFF
) and stores it at the end of the savefile. When loading, it computes the checksum and compares to the stored value. If they differ, the savefile is considered as corrupted and cannot be loaded. To overcome this check, we have to modify values in our save so that the checksum match after the sub-frame reset. The easiest way to change a lot of values is to modify the colors of the different window themes.
Route planning
We investigated the quickest way to collect one of the four items (X-Ether, Gold Hairpin, Czarina Ring, Charm Bangle). The fastest route we found was to enter the Ancient Castle using map warping, where there is both an X-Ether and a Gold Hairpin. To enter the Ancient Castle, we need to save in a map with id
01xx
(we chose the Cave to the Sealed Gate) and in the Phantom Train. To enter the Phantom Train, we need both Sabin and Cyan in our team, otherwise the game will softlock. There are several ways to quickly get Cyan and Sabin in our team:
- Manage to enter Narshe (using save corruption), and reach the snowfields where you can trigger the Kefka fight. At the end, you have a character selection screen and you can place Sabin and Cyan in your team. Estimated time: 6 minutes
- Enter Zozo and climb to the top with Terra. After the cutscene, you have a character selection screen and you can place Sabin and Cyan in your team. Estimated time: 8-10 minutes
- Go to the Imperial Camp and do the whole sequence. You will recruit Cyan and Sabin will be placed in your party as well. Estimated time: 10 minutes
Checksum bug
The initial route we designed involved an early Narshe escape. When you load the game for the first time, it sets all values in the SRAM to 0. We could take this in our advantage because the map id of the World Map is
0000
. By reaching the first save point in Narshe and save/reset just before the map id is overwritten, we could produce a savefile that spawns you on the World Map. Then, we could enter Narshe again and trigger the Kefka at Narshe sequence. However, this did not work because of a flaw in the game programming.
C3/166D: 20D119 JSR $19D1 Calculate SRAM checksum and stores it in $E7 C3/1670: 20EB19 JSR $19EB Determine if save file is corrupted: | C3/19EB: C220 REP #$20 | C3/19ED: A5E7 LDA $E7 Load calculated checksum | C3/19EF: CDFE1F CMP $1FFE Does it match this file's checksum? | C3/19F2: D002 BNE $19F6 Branch if they don't match | C3/19F4: 8001 BRA $19F7 Skip next instruction if they match | C3/19F6: 7B TDC Set A to 0 | C3/19F7: A8 TAY Transfer A to Y | C3/19F8: E220 SEP #$20 | C3/19FA: 60 RTS C3/1673: 8491 STY $91 Save result C3/1675: F00B BEQ $1682 Branch if result is 0: skip savefile
The consequence of this code is that a savefile is considered as corrupted if the checksum does not match, or if the checksum is 0. In our case, because the checksum is stored at the end of the savefile, it will be 0. So even if the checksum is correct, we won't be able to load the savefile.
Actual route
The first option was still thought to be possible at first, by transferring the event flags from one save at the beginning of Narshe to another save after Narshe. However, we could not do it in practice because we could not control the checksum to match between both savefiles. This was due to the fast that we have 11 Moogles joining the party, which modifies a lot of values in memory and greatly altered the checksum. With only the game options at our disposal to modify the checksum we are limited by the range we can access. In the end, we had to drop this quickest choice.
The current route uses the third way of recruiting Sabin and Cyan. Although it may be slower, the Imperial Camp is very close to the Phantom Train, meaning less travelling. Also, Zozo's boss would be difficult to beat quickly because of an underleveled party.
Arbitrary code execution
The setup
As started in the previous section, attacking with one of the four items corrupts the stack by setting the stack pointer to
0x000E
. The game eventually reaches a RTS
instruction, so that it loads the 16-bit integer $0F-$10
and jumps to C1/$10$0F
. In address $0E-$0F
we have the &16-bit battle timer. It starts at 0 at the beginning of the battle and increases by one every frame. In $10
we have a temporary variable that, at the moment of the glitch, can be either 0x0E
, 0xAE
, 0xCE
or 0xEE
. In consequence, we can jump to any address C1/0Exx
, C1/AExx
, C1/CExx
or C1/EExx
.
We are looking at jumping into RAM or SRAM, and this means getting out of the
C1
address bank. We only have limited options to leave this bank: executing jump instructions JSL (22)
or JML (5C, DC)
that use a full 24-bit address; RTL (6B)
is also possible. By examining if there is such instruction in our accessible addresses, we found a good candidate:
$C1/CEC7 5C 6F 60 AE JML $AE606F
This jumps to address
$006F
in SRAM, which is just before Shadow's name ($0071-$0076
). This is not part of the game code but starts at the middle on a instruction. By carefully renaming Shadow's name, we could jump to some other places where we have full control of the values, like the color windows. But there is a drawback to this: address $0F
must contain 0xC7
, meaning we have to wait 14 minutes in the fight.
So we looked at another solution, by trying to increase our range of accessible addresses. Now we looked at any jump and return instructions. Because
$10
and subsequent addresses correspond to temporary variables, we looked at some places where $11
and $12
were modified, so that we can hit another RTS
instruction to jump to C1/$12$11
and reach other places in the C1
bank. We found a really interesting piece of code:
$C1/CE59 A5 0E LDA $0E Loads battle counter $0E-$0F $C1/CE5B 0A ASL A Multiply by 2 $C1/CE5C 85 12 STA $12 Stores in $12-$13 $C1/CE5E 0A ASL A Multiply by 2 $C1/CE5F 0A ASL A Multiply by 2 $C1/CE60 85 10 STA $10 Stores in $10-$11
By choosing carefully our battle counter value, we could control
$11
and $12
and jump to new addresses. Now, we looked again at JML
and JSL
instructions but in the whole C1
bank. We found the instruction JSL $A41E20
at several places in the code (C1/A21C
, C1/A239
, C1/A256
, C1/A276
, C1/A30C
, C1/A32E
, C1/A3CE
, C1/A3E6
). This jumps to the RAM address $7E1E20
which is at the beginning of a unused segment. Like with the teleport anywhere trick, we manipulated the first two values to be 70 98 (BVS $1DBA)
, so that we jump to $1DBA
which is at the end of the menu configuration. Then we change the colors to write in $1DBA
70 A5 (BVS $1D61)
to jump again at the beginning of the menu color configuration, because we won't have enough place to write the full code.
Before writing the full code, we must be sure to be able to execute the above
JML
instruction. If we write the battle counter in binary and look at the obtained values for $11
and $12
, we get:
Battle counter: $0F = ...abcde | $0E = fghijklm Return address: $12 = ghijklm0 | $11 = abcdefgh
So the constraints on the return address are:
- the high byte must be even
- the highest 2 bits of the high byte must match the lowest 2 bits of the low byte
We found one good match which is:
Battle counter: 01000110 11010001 (4A 51) Return address: 10100010 01010010 (A2 52)
The game will first jump to
C1/CE4A
, which will execute the code above that writes to $11
and $12
. Then, when hitting the RTS
instruction, it will jump to the value in $11-$12 + 1
:
$C1/A253 0E 00 A7 ASL $A700 $C1/A256 22 20 1E A4 JSL $A41E20
Thanks to this new setup, we lowered the time to wait to
0x4A51 = 19025
frames, or 5 minutes and 17 seconds. However, because TAS convention times to the last input, we will use this to our advantage.
The code
We need to write a code that will trigger the ending without any input to do. In details, we need to:
- Place the ending sequence as the current event
- Fix the ending softlock that occurs when skipping the transition to the World of Ruin
- Fix the stack pointer value
- Finish the fight
- Avoid any message at the end of the fight that would need to be manually confirmed
- Return to the normal game execution
Also, there are some constraints on writing using window colors: every other value must be < 0x80.
Here is the following code used in this TAS:
C2 20 REP #$20 16-bit accumulator A9 62 13 LDA #$1362 Loads the ending sequence event address 18 CLC Dummy 9D 4E 12 STA $124E,x [$12E5] Set event pointer to the ending 0C 4F 1F TSB $1F4F Set NPC event bit to avoid the ending softlock 69 7F 02 ADC #$027F A = 0x15E1 1B TCS Fix the stack pointer E2 20 SEP #$20 8-bit accumulator 9E 59 3E STZ $3E59,x [$3EF0] Unflag enemy 1 death, to avoid an XP/Gold message 7B TDC A = 0 9E 5B 3E STZ $3E5B,x [$3EF2] Unflag enemy 2 death, to avoid an XP/Gold message 3A DEC A = 0xFF 8D 3A 3A STA $3A3A Remove enemies from the fight 5C EA 4D C1 JML $C14DEA Or any address in the C1 bank that leads to a RTS
The fight
We need to find a fight where eventually the holder of the glitched weapon will attack at exactly 19025 frames after the beginning. There are several way to provoke an attack without any input, the one we chose was to get the character muddled, so that he automatically executes a random command. We found the Goblin enemy in the Cave to the Ancient Castle that has a very interesting battle script:
If 1 or fewer monster(s) (total) is/are remaining: Rand. spell: L.5 Doom or L.4 Flare or L.3 Muddle Rand. spell: Blaze or Nothing or Nothing
Locke is naturally level 6 as we are doing Narshe with Terra level 4 and he gets a +2 bonus when joining, so he will be the holder of the glitched weapon as he is sensible to L.3 Muddle. The battle strat is then to kill all enemies except one Goblin, and manipulate his RNG so that we will cast L.3 Muddle and Locke will attack at the exact moment to trigger ACE. This took about 20 hours of work actually, more than the entire rest of the TAS, but the result is good.
Run comments
Movie creation
We starts the TAS by checking "Random initial state" and setting "Initial RTC value" to
29857
. This will write $1F60 = 9B-CD
for world map teleport and $1E20 = 70-98
for ACE.
Narshe
Nothing special about Narshe itself. We must get Terra to level 4 so Locke joins at level 6. Level 4 Terra is mandatory anyway to get a quick kill on Whelk.
Before leaving Narshe, we save at the beginners class so that the uninitialized values for world map coordinates does not get rewritten. Then, we go up and open the menu so the map Y coordinate takes the value of
0x34
and save. We enter Narshe again and go again at the coordinate X = 0x18
and save/reset on the previous slot so that it now contains the map coordinates 18-34
.
Outside Narshe
We load the save file in the class room and save/reset on the save outside Narshe to overwrite the world map coordinates. By loading the file, we will be placed at coords
9B-CD
in the World Map in the Vector continent.
To the Sealed Gate
We use another world map teleport using Y coord from Narshe save and X coord from Vector to arrive near Gau's father house. We buy here a Sprint Shoes and sell Locke's weapon. We use another teleport to arrive in the bridge to the cave. We enter the cave, grab the Assassin weapon for Locke and make our way to the save point. We save/reset once to transfer the weapon to the savefile around Gau's father house, and another save/reset to write the map id on the savefile that has the map coords
18-34
.
Imperial Camp
We continue our way until the Imperial Camp, and do the whole sequence (almost) normally. We have help from Locke's weapon which has a 1/4 chance of killing the target. This does not work on MTek Armor, too bad.
Phantom Train
We use a save/reset to teleport over the tile of the Forest entrance. This allows us to enter from the exit, and cut a part of the Forest. We enter the train and save/reset to overwrite only the first byte of the map id. By loading this savefile, we appear in the cave to the Ancient Castle at coordinates
18-34
. If we didn't manipulate the coordinates in Narshe, we would appear in the walls out of bounds, and we would be stuck.
Ancient Castle
We enter the Castle, and use the fights to move Locke's Assassin at the bottom of the inventory. We can then equip the Assassin on Cyan using the equip glitch. We grab the X-Ether and leave the Castle. We equip the X-Ether on Locke and a Shield. The shield is mandatory, otherwise Locke would attack with bare hands during the fight and the glitch won't trigger. We write the code to execute by changing the colors in the menu.
Last fight
We kill the two enemies with Cyan, using the wait trick (opening a menu to freeze the enemies ATB during an animation). Then we produce lag in the RNG counter by selecting and un-selecting a weapon, to get the right setup. When we confirm Locke's steal command, the movie ends. After 5 minutes of fight, Locke attacks and the ending triggers.
Special Thanks
- bover_87 for the enemy battle script FAQ
- ff6hacking website for centralizing and showing new data on ff6
Suggested screenshots
Frames 73132
fsvgm777: Replaced with a LSMV that removes an empty SRM file. The total time remains unchanged.
Samsara: (J)udging.
Samsara: This is not the fastest run in the "game end glitch" category. Therefore, we cannot accept it.
I see three big arguments in the submission thread, so let me break them down real quick.
1. "This is a different kind of ACE, so it should be a separate category"
That's not how we do things. This specifically aims for the fastest glitched time. The Nico TAS that's 18 seconds faster than this one (and that's not counting this submission ending input 5 minutes early, but I won't be pedantic about that) also aims for the fastest glitched time. For the purposes of this site, "game end glitch" refers to any way that the ending is reached far earlier than expected. This could be something as simple as holding two buttons on an elevator and landing somewhere, or crashing the game in a way that lets you manipulate your way to the ending, or just going up and down stairs a bunch. "Game end glitch" is exactly how it sounds: The run uses a glitch to end the game. Both this run and the Nico TAS use glitches to end the game. Therefore, they are both "game end glitch" runs. We have a separate ACE branch, but it is only used for playaround runs. Since this was treated as an improvement to the published run (thus, the fastest run) and not as an entertainment-based category, we have to treat it as such as well.
That's not how we do things. This specifically aims for the fastest glitched time. The Nico TAS that's 18 seconds faster than this one (and that's not counting this submission ending input 5 minutes early, but I won't be pedantic about that) also aims for the fastest glitched time. For the purposes of this site, "game end glitch" refers to any way that the ending is reached far earlier than expected. This could be something as simple as holding two buttons on an elevator and landing somewhere, or crashing the game in a way that lets you manipulate your way to the ending, or just going up and down stairs a bunch. "Game end glitch" is exactly how it sounds: The run uses a glitch to end the game. Both this run and the Nico TAS use glitches to end the game. Therefore, they are both "game end glitch" runs. We have a separate ACE branch, but it is only used for playaround runs. Since this was treated as an improvement to the published run (thus, the fastest run) and not as an entertainment-based category, we have to treat it as such as well.
2. "This run is more entertaining, so it should be a separate category"
That is also not how we do things. This is not a playaround run, we cannot treat it as such. The authors intended for this to be an improvement to the published run as the fastest time for this game. Unfortunately for them, a separate team completed a run with a faster time that just so happens to not be nearly as entertaining. Sure, entertainment is a huge focus for us as TASers, but for years now it has not been the primary focus. We do publish new categories if people find them entertaining enough, but not two runs in the same category. And yes, all of these arguments do end up going in circles and coming around to the same point, that the detractors are almost literally saying "publish this run in the same category as a different category".
That is also not how we do things. This is not a playaround run, we cannot treat it as such. The authors intended for this to be an improvement to the published run as the fastest time for this game. Unfortunately for them, a separate team completed a run with a faster time that just so happens to not be nearly as entertaining. Sure, entertainment is a huge focus for us as TASers, but for years now it has not been the primary focus. We do publish new categories if people find them entertaining enough, but not two runs in the same category. And yes, all of these arguments do end up going in circles and coming around to the same point, that the detractors are almost literally saying "publish this run in the same category as a different category".
3. "The faster run is not being submitted, so this should be published anyway"
Pop quiz: Is this how we do things? No. The existence of the faster run is the reason why we can't accept it. It doesn't matter where it exists, how it exists, or when it exists. What matters is that we have it as public knowledge. There's an input file for us to easily verify it. There's an encode for people to watch it. It's there. It's faster. We don't publish RTA runs here, but if a TAS is slower than an RTA run then we reject it exactly because of that. Part of the judgement process is verifying that the run in question is the fastest known record for the game. We're human, we do miss clear and known improvements sometimes, especially ones hosted nowhere except on a OneDrive link that's only shared on Twitter to a small group of people, and yes there are times where we accidentally publish a run before we find a known improvement. But this is not the case.
Pop quiz: Is this how we do things? No. The existence of the faster run is the reason why we can't accept it. It doesn't matter where it exists, how it exists, or when it exists. What matters is that we have it as public knowledge. There's an input file for us to easily verify it. There's an encode for people to watch it. It's there. It's faster. We don't publish RTA runs here, but if a TAS is slower than an RTA run then we reject it exactly because of that. Part of the judgement process is verifying that the run in question is the fastest known record for the game. We're human, we do miss clear and known improvements sometimes, especially ones hosted nowhere except on a OneDrive link that's only shared on Twitter to a small group of people, and yes there are times where we accidentally publish a run before we find a known improvement. But this is not the case.
Trust me, I think this is a great run and I would have loved to see it accepted and published, but this particular run in its current state, as I and others have mentioned several times before, cannot be published, even as a different category. I extended some leniency, hoping that there was a way the run could be salvaged and improved or at least explained in a way that makes it drastically different enough to publish, but over the responses I see in the thread it doesn't look like anything I suggested is going to be met. There was a slight ray of hope in the Nico TAS starting from dirty SRAM, but looking at the file more closely the SRAM is blank, much in the same way that this submission was at first, so it turned from a ray of hope into the final nail in the coffin.
Rejecting for not beating the fastest known time.