Remote Control Boomerang

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. Powder uses it as well. And, for some reason, instead of running the routines itself, the lamp creates a powder ancilla to run collision checks. This is why the lamp is so powerful to set $03C4.

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. This is a mess of a routine that I won't bother trying to explain. In short: it's an inefficient 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. Using the built-in registers would pretty much always have the same run time. And that run time will always be faster than this loop.

As is, the function runs in O(n) time, when it could have run in O(1).

Tackling the real issues

The problem we're interested in, specifically, is how the loop is terminated. And to be precise, we care about when the loop counter is exactly 0. This is fine in most cases. The routine is basically a copy/paste of the sprite version, which does find use for 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.