Mirror Block Erase

Erasing pushable blocks with the mirror is something everyone knows how to do. Just use the mirror while the block is moving, and POOF! the block is gone. I don't think I've ever seen an earnest attempt to explain why it happens; everyone just accepted that it does. Well, gang, the mystery is now solved. And the explanation is super simple, to boot.

Blocks

Let's start by understanding how pushable blocks move in the first place. Only 2 blocks are meant to do anything at any one time. To handle this, two addresses ($05FC and $05FD) hold the index+1 of the currently active blocks—more on this weird indexing later. A value of 0 means that the slot is not active. If blocks can't find a free slot to use, they simply won't get pushed.

Now let's look at the routine that is executed every frame in normal dungeon mode:

  1. The index loop variable (address $042C) is reset and then the routine PushBlock_Handler is executed.
  2. The value of $042C is loaded into Y. It is compared to the stopping point (address $0428). If they are not equal, then the state of the block at that index is checked, otherwise, the routine exits.
  3. The desired block's state is read by using Y to index the array at $0500.
    • If the block's state is 0, then it is inactive, and the the next block is looked at.
    • If the block's state is 1, then it is erased from the tile and collision maps. Its state is incremented to 2, and the next block is looked at.
    • If the block's state is 2, then its movement code is executed. Afterwards, the block's state is checked again.

      If it is 3, the block's state is quickly incremented to 4. This check for state 3 is inside the state 2 branch, so it will only ever be checked from there. This state appears to be an indication that the block is over a pit.

      If the block has finished moving without reaching 3, it resets its state 0.

    • If the block's state is 4, then it is falling, and that animation is performed. When the animation is finished, the block is reset to state 0.
  4. Address $042C is incremented twice, then the loop is started again.

State 2 is where the problem occurs in most cases, so that's the branch we'll focus on. But actually, you can mirror erase a block that's newly pushed (state 1) or falling (state 4). The reason will end up being the same.

  1. State 2 begins by calling a routine that doesn't have a name in the disassembly, but it's $3EDB5 in the US ROM file, so let's call this routine Bingo, because it ends with B5.
  2. Bingo goes like this:
    1. Check the current submodule. If it isn't 0, exit the routine.
    2. Cache the value of Y in scratch space.
    3. Set X to 1 and use X to index address $05FC, loading that value into A.
    4. Decrement A by 1—I promise I'll get to it—, multiply it by 2, then compare it to the cached value of Y.
      • If A and Y are equal, then leave X at 1 and continue.
      • If they don't match, set X to 0 before continuing.
    5. Multiply X by 2 and give it to Y.
    6. Do moving and stuff.
  3. Continue the movement routine using Y as the index for the block's data.

Hold the phone. What?

Why are you using X to index an address outside of a loop, right after you've loaded a constant into X? That will never not be $05FD.

Why are you loading 0 into X and just rolling with it? I think what you meant to do was branch backwards and run that indexed address check again.

These are questions that people want answered. At least I do!

The reason active block indices are stored as index+1 is so that 0 can act as an indicator that means the slot is empty. The reason the value is doubled is because that value indexes bytes; the routine needs to index words, which are twice the size.

Rolling with slot 0 allows the possibility of operating on an index of 0. What if a block is moving but doesn't have its index stored? Can you make that happen?

Amazing Mirror

You sure can!

Only a few things reset those index addresses:

You get where I'm going with this already? Well, shut up and let me finish.

The developers actually seemed to know that blocks resetting the indices themselves wouldn't be enough. They tried to cover all their bases by examining what could be done if a block is active. They covered most of it. Just me personally, but I would have made them reset whenever a new room is loaded. But I digress.

I digress pretty often actually.

The developers also made an itsy-bitsy mistake in JP1.0: they had the mirror clear the slots every time it is used. Later versions fixed this by moving those clears to be inside of the "successfully mirrored" branch.

In a way, you're not actually deleting the block, you're just making it behave as if it's somewhere else. In fact, the block remains active after you mirror erase it. It will continue its routine forever, because there's no one telling it to stop. The values it's being told to use are all wrong, and it won't reach its expected destination anymore. It just chugs on, oblivious to the damnation cast upon it.

Its index is no longer in the active block array, but it doesn't need to be. That's only used because the developers also wanted to keep the velocities and other values for moving blocks in a smaller portion of memory. A block runs code regardless when its state in the states array is nonzero. That array is not limited to only 2 blocks. The active index array only tells a block what data it should be using to do whatever it's doing. You can conclude that if multiple blocks have been mirror erased, then they're all messing with the same values. I bet things would look bonkers if we could actually see what was going on!

Summary

When blocks are pushed, the index of their data is put into a slot in a 2 item array. This slot is then used to update their coordinates and velocity. When the mirror is used, both values in that index array are set to 0, which is meant to indicate the slot is free. This happens in caves, but it doesn't affect the blocks' actual status; the blocks will keep running, but a coding mistake will have them assume that they should use the first value in the array as an index if the second value is not correct. This bad assumption results in 0, an invalid index, being used for the block's data.

The block continues operating, and is not technically erased. This makes this glitch a form of misslotting, but a slightly different species from other known misslots. In versions after JP1.0, a check is performed to see if the beep sound effect was played, and doesn't reset the array if it was.