WEST SOMARIAS
If you've ever seen me talk, you've probably seen me say "west somaria" (possibly in ALL CAPS). That's because I'm always excited about them. They open doors! Sweet! West somarias are nowhere near as damaging as the other directions, but they are way cooler.
The culprit you least expect
West somarias don't just magically open doors (actually, I guess they kinda do). What they're really doing is corrupting the game submodule (SUBMODE) held in address $11. This is caused by double triggering the door transition, which is changed with an increment instead of directly setting it to some value.
Why does pushing a block in a door cause a double transition? When you first move, you are hitting the transition like normal. Pressing a parallel direction puts Link back in door state which then allows door transition detection code to run. This is a normal subtask that occurs as part of the camera update routine run every frame. But somaria blocks are special. Interaction with them is handled outside of all other Link code. Seriously! Look at this:
JSR LinkControlHandler
All other Link code
JSR HandleSomariaAndGraves
WOW!
That's not the cause of anything relevant to this article, though. It's just what makes the blocks special.
What is relevant here is that this routine calls a function for completely aborting Link's movement when he is pushed back by a somaria block. This routine cancels an active hookshot, undoes any movements Link may have made, and recalculates the camera. The camera recalculation is done with the same routine run every frame—the one that can trigger transitions in a door. During a somaria transition corruption, Link is still in a door! That means the camera runs another transition check. In other words, two transitions are being triggered: one by our everyframe duties and one by the movement abortion.
Doors 101
But why are west somarias special? Every other STC direction enters submodule $03, because they are double-hitting their transitions—an intraroom followed by an interroom, or vice versa. WEST SOMARIAS are unique in that they hit a west transition followed by an east transition. Both of these are considered an interroom transition; each double increments SUBMODE, putting it at $04. Less cool west somarias work the same way, but they're doing a double intraroom transition, resulting in submodule 2.
The reason for this difference in behavior boils down to the parameters used for detecting transitions. Each direction has its own range of coordinates where transitions can be triggered, but the logic for them is otherwise identical.
Address $69 contains the variable DIFFXH, which stores the difference between Link's X-coordinate high byte before and after movement. This variable and its counterparts are calculated by subtracting from Link's current position his new coordinates held in CALCX and CALCY.
The door transition detection code exists at $07E8EA, where it begins by clearing DIFFYH and DIFFXH. It then checks the direction Link is moving and the axis of the door flag to determine which direction of transition should be attempted.
Next, a prospective coordinate is calculated by looking forward from Link's current position some number of pixels—18 north, 24 south, 21 east, 8 west. The high byte of this prospective coordinate then subtracts the high byte of the cached coordinate held in the respective CALCX/Y variable. The difference is stored in the respective diff variable. If this difference is zero, Link will not transition on this axis; if it's negative, Link will transition north or west; otherwise, he will transition south or east.
As the actual transition is performed, Link will be adjusted some number of pixels forward. West and east transitions will be adjusted forward 8 pixels; south, 16 pixels; and north will be unadjusted. For the latter 3 directions, the new position will be within the same quadrant. For west, they will be one quadrant over.
CAUTION: Not for use in doors
When stopping Link, the somaria code stores his coordinates (which are now the post-transition position) in CALCX/Y and pushes Link back to his coordinates from the start of the frame. This detail is critical. At the beginning of the frame, because of how we snap into doors for a west somaria, Link's X-coordinate ended in $08. When his movement was handled, he moved either 1 or 2 pixels to the west. Let's just say 2. Now his X-coordinate ends in $06. This occurs before any transition detection code. So, for the normal transition, his prospective coordinate will be in the next quadrant.
When pushing Link back, the somaria block doesn't just undo the adjustment done by the normal transition; it also undoes the movement effected by the player. Now Link's coordinates are back to ending in $08. When the abort movement transition tries to determine which direction to go, it will calculate a prospective coordinate 8 pixels to the west. That's just barely still in the same quadrant. Nothing between now and the somaria block's pushback has modified CALCX; it holds a set of coordinates that are in the quadrant to the west. DIFFXH is found to be 1—a positive number. It has been determined that Link should transition east.
Basically, the first transition is calculated normally while the second transition is done with the positions more or less swapped. This flip-flopping explains a lot about west somarias. It's why they have a stable camera. It's why they don't change the room ID. And it's why Link pops backwards after pushing the block.
This is a very precise glitch. When the somaria block is pushed, only 2 positions can effect a west somaria. Luckily, one of these positions happens to be exactly where Link lands during setup. If Link were on, say, an X-coordinate ending in $06 after setup, then there would be no west somaria. The normal transition would trigger correctly, but the abort movement transition would calculate 0 for DIFFXH. No second transition would occur.
So why are east somarias not experiencing the same thing? Well, that's because these transitions occur much sooner. Link's coordinates are defined as the top left of his existence, so anything checking to the right of him on screen needs to account for his width. East transitions are triggered from X-coordinates ending in $EB to $EE. Link will still be adjusted 8 pixels forwards, but, importantly, this adjustment does not push him into the next quadrant. Thus, the calculations by both the normal transition and the abort movement transition result in a positive DIFFXH.
East doors can also experience the null second transition. If Link attempts to push a somaria block when his X-coordinate ends in $EA, the block does actually run a second transition detection. But the pushback will have sent him to an X-coordinate far enough west that the prospective coordinate of that transition will remain in the same quadrant. Thus it yields 0 for DIFFXH. No extra transition.
And what about vertical transitions? North doors just don't adjust Link's coordinates at all. If they did, they might have experienced the same flip-floppy behavior as west doors. South transitions, like easts, occur earlier to account for Link's height and thus don't place him in the next quadrant when triggered.
When is a door not a door? When it's ajar!
So…summary: somaria transition corruptions occur because of the camera, and west somarias are actually west transitions combined with an east transition, because Link is fat.
Now it's time to see why doors open, sometimes after waiting forever. If you want to know what's happening with the other directions, you'll need to read my Overlay corruption Explication.
Submodule 4 is the door unlock submodule. Literally all it does is call UnlockKeyDoor. This routine, true to the name I gave it, animates the opening of a door and flags it as being permanently open. While the entry point is specifically for key doors, it functions just as well on any other type of door. In fact, it shares most of its code with the OpenCrackedDoor routine.
UnlockKeyDoor requires two variables to be set upon entering: DOORPOS at $068E and DOORTIME at $069E. It goes without saying that the intended triggers for this submodule set those values ahead of time.
I sword of understand
The location of the door to animate is found by following a collision map position stored in DOORPOS. Any action that might cause a door to change appearance will use this variable at some point. The easiest method of setting DOORPOS is slashing a closed door. Doing so will cache its location just in case it was a vine door.
Ways to set DOORPOS:
- Slashing a closed door.
- Touching a small key door with a key.
- Touching a big key door.
- Bombing open a wall.
- Operating shutters.
- Intraroom transitions.
Doors that can be opened have a tile collision type of $Fx, where x uses bits 0–2 for the door's index and bit 3 to identify whether it's the top or bottom. All other doors use a generic tile type. To find their tilemap position, DOORPOS is used to locate an entry on the collision map. The lower 3 bits of this entry can be used to index the DOORXTMAP array at $19A0 which contains the door's base position. This allows any part of the door to correctly locate the door as a whole.
The shutter door opening routine also uses DOORPOS, but as a loop counter. It ends up setting it to a value of $0016. Shutter doors are looked for on every intraroom transition, also resulting in this value.
Almost invariably, $0016 points to a tile that behaves as solid collision. In fact, only three rooms don't have solid collision there: Desert Palace lobby, Eastern Palace fairies, and the Hyrule Castle Dungeon catwalk. Solid collision is tile type $01, so basically every room will—if you have $0016 in DOORPOS—look at door index 1. Desert Palace Lobby will use door 0, its entrance; the fairies will use door 4. The castle dungeon catwalk will also use door 4, but that door doesn't exist. This room has no doors; you can't even west somaria here.
The locations array is cleared every time a room is loaded, so if DOORPOS somehow finds an index for an invalid door, the game will determine it's a northwards door at the top-left of the room. Funnily though, there is also a list of valid exit doors in an array at $19E2, with each entry holding a tilemap location. Doors compare their location to each of these entries to determine whether or not they are an exit. At least one of these four entries will be $0000, because no room has more than 2 exits. Thus, this misplaced door is determined to be an exit, and it will behave as one. In a room with a real exit, said exit will be found and used; but, if there isn't one, the game just hardlocks in an infinite loop.
Tick Tock
Speaking of infinite loops!!! Let's talk about dollar six ninety.
DOORTIME is a 16-bit timer that gets incremented every frame with each invocation of the submodule. Its value is checked for being 4, 12, and 16, each marking an update to the tilemap to animate the door. In the first step, on frame 4, the collision map is updated. On the second step, the door gets flagged as opened. On the last step the new door is finalized and SUBMODE is reset to 0 for player control.
The increment occurs at the beginning of the routine and only ever checks for exact equality. This means that performing a west somaria with a value of $0010 will not immediately terminate; rather, the game will wait for this timer to overflow and wrap around back to 0 before continuing as normal. That's a long time. 65520 frames, to be exact—a little over 18 minutes.
To bypass this, we can perform and avoid specific actions to carefully manipulate DOORTIME and keep it low enough for practical use:
- A console reset will set the timer to 0 as it clears all of memory.
- Opening a small key door, big key door, or broken wall will end with a timer of 16.
- Any shutter animation will end with a timer of 16.
- Interroom transitions will not modify the timer. Unless there are any shutters on the side of the room Link transitions on.
- Intraroom transitions will end with a timer of 4. Unless there are any shutters in the room.
- A shutter operation that fails to find any shutter doors to animate will end with a timer of 4.
- Touching a big key door without the big key will set the timer to 0.
- Mirroring on the same frame that a key door is touched with a key will set the timer to 0. The key will also not be used.
- Setting off an exploding wall will set the timer to 0.
- Opening the big doors to Hyrule Castle or Sanctuary will end with a timer of 3.
- On the overworld, lifting big rocks or breaking bonk rocks will increment the timer by 1.
- Opening the grave to sewers or king's tomb or solving the hammer pegs puzzle for the first time will increment the timer by 1.
Those last two items share some code with the big overworld doors, which is why they increment the timer. They never do anything with the value, though. Just be cautious to not break a bunch of rocks when manipulating this timer.
Locked and loaded
Shutter doors come with extra baggage. While they can flagged, double-sided shutters have to meet extra criteria to be open upon entering a room. And, though easily manipulated, single-sided shutters aren't always passable, despite being visually open.
First, no door in index 4 or higher can be permanently opened. Door opening is temporarily marked in the variable OPENED at $068C and permanently in UNLOCKED at $0401. While it is possible to flag any door in either of these, only 4 bits from UNLOCKED get copied to or from the save file.
If the variable SHUTTER at address $0468 is set, the room load routines will not allow any double-sided shutter to open. Single-sided shutters are not checked for. Presumably, this is because the logic is intended for interroom transitions, and all interroom shutter doors are double-shutters by ID (the partner door for these positions is just discarded). And this is how you can get a double-sided shutter open: enter the room via scrolling transition from a wall that has a shutter. This will clear SHUTTER, preopening every shutter door so that they can be animated closed together.
The OperateShutterDoors routine includes a check to ignore any door flagged in OPENED; although, this only works once. Once is usually enough to reach the shutter door while it's opened, but the next time a global shutter door operation occurs, even permanently opened shutters will comply. They'll stay permanently open, though; if the room is reloaded, the beginning of this paragraph will apply again. UNLOCKED is never modified by shutter operations.
Double-sidedness is also why single-sided shutter doors are finicky. DOORXTMAP and other door arrays contain information in 2 halves: the true door and its partner, with the latter held 8 slots behind the former. Only the true door is flagged in OPENED. Shutter door updates check OPENED for the corresponding slot's flag to determine a shutter door's collision, but this flag is based on slot. Partner doors will have a high slot whose flag is never set. This is why, for example, the shutter door in the Ganon's Tower invisible floor maze can be west somaria'd then used immediately, but the one to the big chest requires the room to reload to become passable.
Another cute quirk with one-sided shutters is that permanently opened doors have both sides replaced with the same door type, which is found via table at $009A02. To reuse everyone's favorite as an example: the shutter door blocking the Ganon's Tower big chest will become a shutter on both sides after reloading the room.
As a rule of thumb, intraroom doors always have the main door on the north or west walls, with their partner on the south or east walls, respectively.
Collaboration
In theory, we can use overlay corruption to put arbitrary tilemap positions into the DOORXTMAP array allowing us to index the tilemap out of bounds with a west somaria. This needs to all occur in the same room, and that room needs to naturally have far-reaching corruption. Even when you've found such a room, it needs to have specific room objects to do anything. Every entry of DOORXTMAP will be populated by tile values from the current tilemap, which puts specific limits on where they can potentially write.
The tilemap's base address is $7E2000, and it occupies $4000 bytes in memory. Following that is a decompression buffer that occupies $3000 bytes and then decompressed graphics for another $3000 bytes. The first interesting address is the room warp destination at $7EC000; hitting this would require a tilemap value of $A000. That's not a value you will ever find in the tilemap. How do I know? Because I can just look at this gigantic table in ROM that contains every tilemap value used by any object in the underworld.
Instead of speculating what might be possible, how about I peruse that table to see if any of the values correspond to interesting memory? Is there anything cool?
The answer is no.
Technically, if it were in a better room, the vitreous goo could help convince the game you've lifted some non-existant pots in Ganon's room. And these pit corners could corrupt the location of lightable torches until you reload the underworld! But who cares?
What about the collision map? That's also updated by door opening routines.
The collision map sits at $7F2000 and occupies $2000 bytes. Changes to it are limited in how far they can write. The collision map is half the size of the tilemap buffer, so tilemap positions are divided by 2 to index it. They may also be masked, which limits them further. Most of what follows the collision map is boring or nothing, but two interesting, procedurally-built tables are within range: enemy damage classes at $7F6000 and message pointers at $7F71C0.
Wow! This could be huge! Imagine what possibilites there are with the ability to modify the damage enemies take!
There's nothing cool.
I asked jsd to help me find every room where overlay corruption populates DOORXTMAP with interesting values. He reported back with several, but a lot of the candidate rooms were just not viable for glitching. To work, they need to have an appropriate configuration of doors as well as navigable post-overlay corruption collision. There also needs to be a way to set DOORPOS such that the desired door can be opened with a west somaria.
Instead of struggling to make things happen, let's go back to theory. We'll assume all of these candidate doors are easily twofold-corrupted and just look at what's theoretically possible. There's still not much of interest to talk about.
The enemies whose damages get changed are whatever. There are some odd sprites like somaria platforms or wall cannons that can be affected, but they can't even be damaged, so it's irrelevant. Well, maybe you can kill a cannon with arrows. I haven't checked, and I'm not gonna.
There's also the problem that we mostly get door-related tile types for the damage table writes. The damage subclass lookup exists at $0DB8F1 in ROM and is followed by a 256-byte table dictating how projectiles should behave; this table only contains values up to $04, which means out-of-bounds damage table indices will return very low damages or zero. The indexed-door tile types of $Fx also dominate the calculation when treated as a subclass due to how the index is found, and all but one of them will fetch a damage of $00.
Illegal door types derived from the corruption in other arrays might result in some more exotic tile types being written, but none of the candidates are interesting enough to investigate further.
Just to do an example: we can try twofold-corruption in the Thieves' Town hellway. A west somaria here will include a write of $F9 to $7F6446. This will change the fast blue guard's damage-taken-from-arrows subclass. When finding the actual damage to inflict, the lookup shifts the damage class three bits to the left before adding in the subclass, but that makes it irrelevant, because bits 3 through 7 are already set in the subclass. The routine thus uses an index of $F9, reading $0DB9EA to get 0. Now arrows do no damage to these guards.
Really? This is the most potent data corruption we have, covering a range of over 90,000 bytes! But, in practice, it's completely useless. What a shame.
For the curious, I've created a table of the candidates and what memory they might technically be able to corrupt.
Ideally, Ganon or Agahnim could have been modified to take more serious damage, possibly from a different weapon. Being able to set Ganon on fire with the boomerang would have been cool and maybe even redefine the swordless category.
With the message pointers, we were crossing our fingers to hit one of the crystal maiden messages. It could have potentially cut minutes off of glitched speedruns by turning their blabbering into short gibberish. Unfortunately, very few messages are hit. We can change the spawn point selection to say "集めるのです。", which is funny, but not funny enough to justify. Especially in a speedrun.
These were small targets, so the chances of something useful were low to begin with, but, like, could you imagine if?
Summary
Somaria transition corruptions occur because the somaria block pushback routine includes a very haphazard movement halt that recalculates the camera, causing a double transition. West somarias are unique due to coordinate manipulation that occurs on transition. This difference causes them to perform a west transition followed by an east transition. These opposing transitions enter the key door opening submodule, allowing arbitrary doors to be opened by carefully manipulating the timer and position cache used by this routine.
Due to extra logic surrounding their behavior, shutter doors have some quirks to consider when targeted. They can be visually open but impassible, and are picky about when they stay open. They also have the ability to infect the door they're paired with, turning it into a shutter when the room is loaded.
Combined with overlay corruption, west somarias have the potential to corrupt the largest known range of memory; however, most of this range is not interesting, and the memory that can be corrupted in practice is not really useful.