Remote Control Boomerang

STOP THE PRESSES!

I guess, in this case, they're key presses. But that's besides the point. There's a hot new glitch discovered this morning, the 15th of February 2020. And we're the first on the scene.

What's the scoop? It's easy. Get a boomerang into slot 0 or 1, then get an ice rod shot into slot 3 or 4, respectively. The boomerang stops moving on its own, instead following a weird trajectory as Link moves around.

Velocity

Velocity is pretty easy to understand. Add this number (speed) to that number (coord). Hooray! We moved! Calculating velocity is not always as easy to understand. Sometimes, you need to use trigonometry. In the case of A Link to the Past, you need to use something that poorly mimics trigonometry. For the boomerang, or indeed, ancillae in general, the routine of interest is Ancilla_ProjectSpeedTowardsPlayer. It will be covered more in-depth later; all you need to know right now is that before calling this routine, some value is loaded into the accumulator. This value is the magnitude of the base speed to be manipulated into a 2 dimensional velocity.

The boomerang turns around. Otherwise it'd just be a stick. The throw speed and base speed are calculated on initialization. A magnitude of $20 or $30 is used for cardinal throws, and a value of $18 or $28 is used for diagonal throws, for the blue and red boomerangs, respectively. If a velocity is for moving up or left, then its value is made negative. Diagonal velocites are put on both axes; obviously, cardinal velocities only go on the relevant axis. Regardless of direction, a positive number—the absolute value of the speeds used—is used for the return speed, which is stored in $03C5,X. This value is later used when calling Ancilla_ProjectSpeedTowardsPlayer to create a velocity on each axis every frame.

Weird… why should diagonal throws return slower? Whatever.

The boomerang will glide along happily if it was just thrown. It's not until it's reached its maximum distance that it suddenly stops. Something must be setting the return speed to 0.

As far as the boomerang is concerned, nothing abnormal is actually happening. It's chugging along and executing all of its code perfect. What a good boy. That doesn't change when this glitch takes place. Clearly, the ice rod is the problem. More specifically, the ancilla it creates.

Sharing is erring

We've already got our finger pointed at the ice rod, so let's see what's up. The answer is a bit subtle, but it's a single operation in the ice shot initialization code: STZ $03C2,X. In our example, the ice rod is slot 4, so that'd be $03C6. Hmmmmm… If the boomerang is slot 1, that means… Aha! The return speed of the boomerang is the first slot of $03C5,X, which is also $03C6. The ice rod is using a shorter space as if it were longer, which causes it to bleed over into memory already allotted for something else. Case closed! And, later on, it uses this value for absolutely nothing.

That's right! That damaging operation had no function whatsoever. If it were removed, ice rod shots would act no differently, but the boomerang wouldn't be subject to this glitch. Or would it…?

The ice rod isn't the only thing that messes with this array. In fact, there's another array that's only got space for 2 elements right before it. And that array is responsible for somaria lamp bouncing. Somaria blocks also use $03C5,X, but they use it to control bouncing, which you probably figured out from the preceding sentence.

RC

So we have that much figured out. Most people would be satisfied with that. Not me. Sure, we know why it stops, but what about how it moves? Why does it sort of move with Link? Why does it seem to move away from him?

To answer these questions, we need to take a look at Ancilla_ProjectSpeedTowardsPlayer.

  1. Cache the base speed in scratch space. Save the X and Y registers in the stack.
  2. Do a bunch of weird garbage to see whether the ancilla is farther from Link on the X-axis to the left or to the right. Remember the absolute value.
  3. Do similar weird garbage to see if the ancilla is farther from Link on the Y-axis to the up or the down. Remember the absolute value.
  4. Zero out an address we'll use to collect numbers later.
  5. Zero out an address we'll use to build speed for one axis.
  6. Load the base speed we cached earlier into X.
  7. Load the collection plate, and add the displacement on whichever axis was closer to Link. Compare it to the displacement of the farther axis.
    • If this first sum is less than the second value, just continue to the next step.
    • If this sum is greater than or equal to the farther axis' displacement, subtract its displacement from the sum, then increment the speed building address by 1.
  8. Use the final calculation from the previous step to update our collection plate. Decrement X by 1. If it's not 0, go back to the previous step.
  9. When we're done, whichever axis had the lower displacement that frame gets the collection plate. The farther axis gets the base speed.
  10. Correct the direction of these new speeds, based on the direction they were displaced from Link.
  11. Recover our X and Y registers from the stack and return.

So what does this mess actually do? Well, it's an over-the-top algorithm to calculate b×tan(θ), where b is the base speed given as an input and θ is the angle of a right triangle whose hypotenuse is a line that connects Link and the entity, with the side adjacent to θ being longer or as long as the side opposite to θ.

Or to put it less formally: base_speed×(smaller/bigger).

I don't think I get it either. I mean, I get it. But why? That's a really weird way to go about doing this. It doesn't even partition the velocity cleanly. One of the axes (the further one) will always have the input value. The other will have something else. This results in values anywhere from b to b√2 as the final gross speed, which shouldn't be desirable. The more reasonable expectation is that the gross speed is the magnitude of the sum of each axis' vector.

The worst part, though, is that all this is doing is multiplication and division. While there may not be any operations in the CPU's instruction set for these functions, there are hardware registers specifically for that purpose. Very small base speeds will outperform those registers, but values are never that small. By using this looped algorithm, the runtime becomes longer as the base speed increases. Using the built-in registers would pretty much always have the same run time.

If you want to see an example problem worked through (and you don't), check out this page.

Tackling the real issues

The problem we're interested in, specifically, is how the loop is terminated. It's when the loop counter is exactly 0. This is fine in most cases. The routine is basically copy/paste from the sprite version, which does find use negative values to move sprites away from the player. But in the case of 0, it's acting like a big negative value, but only for the loop.

Oh wait, here's a funny thing: the sprite version of this routine does not accept values of 0. Someone must have realized it could be a problem there, so the routine exits immediately.

The more displaced axis will always get 0. The other axis will get a speed that locks it onto 1 of 8 lines, which are 45° (π/4 rad) apart, starting from 0. Once the boomerang has reached a point on this line, it will stay there until disturbed. If it has to move, it will favor one of the directions it was thrown in, preferring Y over X when both are equally displaced.

In some sense, the boomerang no longer cares about returning to Link. It will return if you can reach it, which is possible if you move towards it along the same line it's obsessed with. The boomerang normally seems to be moving pretty slow with this, but that's only because it doesn't need to move much to stay on the line. If you move closer to it in one direction but not the other during a diagonal throw, it will achieve high instantaneous velocities as it snaps towards a cardinal line.

Summary

Data bleed causes the ice rod shot to set the boomerang's return speed to 0 when it's in a slot 3 positions lower. Funky speed math causes the boomerang to snap to and stay on specific lines, which all intersect at Link. This funky math also involves an overflow from the value of 0, resulting in negative numbers that tend to push the boomerang away from Link.