Heart dupe

If you've ever been desperate for health, you might have used the hookshot or boomerang to grab a heart with the hopes of getting double what you bargained for. And if anyone's ever told you that this glitch does not work with rupees, etc, well… that's mostly true, but there's an extra item you can double up.

Absorbables

Unsurprisingly, everything a sprite can drop is grouped together (at least categorically by MathOnNapkins) into a class called "absorbables". Rupees, arrows, bomb refills, magic decanters, and stolen shields all share the exact same routine. Big and small keys share the routine as well, but with a little extra logic beforehand. Fairies and hearts each have their own main routine, but share many subroutines with the rest of the absorbables. Believe it or not, apples are completely unrelated, and share none of the same logic. Apples can't be dropped by sprites, so my previous statement is still correct.

Did you know you can't actually collect apples with the boomerang? I bet you thought you could.

Arnold

Before we look at the actual code, let's discuss a basic computer science topic: stack unwinding. To explain the stack as succinctly as possible: you can push and pull; the most recent item pushed will be the first thing pulled. The stack is used for calling subroutines so that the CPU knows where to return to when the routine is exited. The stack is the only reference it has to go by for this, so if the topmost item changes between the time the routine is called and the time it is exited, the CPU will return to a different location than where it started.

Stack unwinding is doing exactly that, but on purpose. I can use sprites (which hearts and rupees are) as an example. Sprites themselves are subroutines, named in the disassembly as "Sprite_X". A common routine among most sprites is CheckIfActive. This routine will see if the sprite should actually perform its AI. If it should, it just returns. If it shouldn't then this routine unwinds the stack. Doing so removes the return address for CheckIfActive. When it jumps back, it returns to where the sprite was called from.

LDA $0D00, X
Get AI state
CMP #$09
Are we active?
BNE inactive
Not 9? Not active.
LDA $0FC1
Global pause flag
BNE inactive
Set means inactive
LDA $11
Check submodule
BNE inactive
Filter non default submodes
LDA $0CAA, X
Off screen activeness flag
BMI active
It's in bit 7
LDA $0F00, X
Sprite specific pause
BEQ active
Unset means active
inactive
PLA
Unwind the stack
PLA
active
RTS
Exit subroutine

Om nom

There are 2 ways for absorbables to be consumed: touching them directly, and via ancilla. These are handled by TestAbsorption and HandleDraggingByAncilla, respectively.

TestAbsorption is simple. First it checks $0F10,X, which is used to indicate the sprite is a bonk item. If it is a bonk item, then this routine exits without checking for absorption. If the flag isn't set, the sprite's hitbox is checked to see if it overlaps with Link's, exiting if the result is failure. When the hitboxes overlap, the routine GetAbsorbedAsItemLong is called to perform the consumption logic.

And that should not be a long routine. Long routines should be used when the caller is not in the same bank. In fact, this routine shouldn't even exist. Everything that does call this is in the same bank. What a waste of cycles!

Anyways…

GetAbsorbedAsItem first kills the sprite by setting its state to 0. Then it uses the sprite ID to index a table of sound effects and a table of consumption logic (what exactly is being restored).

HandleDraggingByAncilla is a bit different. When a sprite is being carried by an ancilla, the index of that ancilla (plus one; zero means no drag) is stored to $0DA0,X, a property of the sprite. That value is then used to check the ancilla's properties. The first thing checked is the ancilla in that slot. If there's nothing there, the sprite is absorbed. And that's actually it. That's the only logic used to check if a sprite should be consumed when its on an ancilla. GetAbsorbedAsItem is used here as well.

When the ancilla is active, its coordinates are copied to the sprite's coordinates. Also, the sprite's altitude is set to 0, regardless of the altitude of the ancilla.

In both cases where a sprite is being dragged—the ancilla is active, or inactive—this routine will unwind the stack. The rest of the sprite's code is skipped, because the CPU returns not to the sprite, but to whatever called the sprite.

Order of operations

The majority of sprites, which all share the same code, perform the routines in this order:

JSR HandleDraggingByAncilla
JSR TestAbsorption

Since HandleDraggingByAncilla unwinds the stack, it will prevent GetAbsorbedAsItem from being run.

Hearts, on the other hand, be like this:

JSR TestAbsorption
JSR HandleDraggingByAncilla

These look strikingly similar, but in my explanation of GetAbsorbedAsItem, nowhere did I mention stack unwinding. And that's because it doesn't. For hearts, the first absorption check can be successful, but it will not prevent the second check from happening. So if you happen to have your hitbox overlap with a heart on the correct frame rule and on the same frame that the ancilla carrying it disappears, the consumption logic is run twice. The healing is additive, allowing it to stack with other healing effects that occured on the same frame. Including itself, apparently.

So why are hearts different in the first place? The big bunch of absorbables all fall to the ground the same way, with a jump and a bounce. Hearts take a more gentle approach and drift down like a feather. This required custom movement logic, and, at that point, it made more sense to handle everything separately. Somewhere along the line, someone switched the two routines.

On top of that, the main bulk of absorbables are running the checks at the end of the main routine. Hearts do it at the beginning. If a heart is collected while not on an ancilla, it will technically keep moving for the rest of the frame, despite already being deleted.

And that's because killing the sprite doesn't stop its code. This does not take effect until the next frame, where the state will be read as 0, causing the slot to be skipped. Of course, those other absorbables also move on the same frame they get eaten, as it happened beforehand. But I think it'd be nice if those cycles weren't wasted. If you ever somehow lag while collecting a heart, feel free to blame it entirely on this inefficiency.

Double Decker!!

OH, BABY! Two explications in one post?!?!?

I noticed something else while writing this:

And that's why bonk prize pre-grabbing works. The sprite is attached to an ancilla, and then the ancilla is gone. From there on, it will be attempting to vore itself every frame, but failing because it's in the bonk prize state. As soon as you release it from that state, it will be consumed.

And in writing this, I realized you didn't have to stop there. These sprites don't care what ancilla is there, just that there is one. If before bonking the item free, you spawn a new ancilla into that slot, the sprite will happily attach itself to that one. Unfortunately, it will still run its countdown timer to despawn, but it's really fun while it lasts.

Fairies

Fairies are an odd case. They perform the same order of operations as hearts, but you may not have ever doubled a fairy. That's because the hitbox of fairies makes it exceedingly difficult. You'll probably never do it from a standstill; it's not common even if you're trying. But it is in fact possible, resulting in a whopping 14 heart heal.

Summary

There are 2 ways by which absorbable sprites can be consumed: from player contact, and via ancilla. For most absorbables, the ancilla routine is run first. The ancilla check just looks to see if the associated ancilla slot that the sprite has latched onto is occupied. This routine contains a stack unwind to prevent the rest of the normal sprite code from being executed. Hearts and fairies run the player contact check first, and that routine does not unwind the stack. This means the rest of the code, including the ancilla check, can be run. The player contact is based on standard collision checks, so it is subject to frame rules. This means that heart duping is effectively a 1/4 odds of success. Fairies have a weird hitbox that makes successful contact on the same frame that its ancilla disappears difficult. It may only be possible with the boomerang.

Pre-grab works because the bonk prize state is unique to absorbables, and not subject to the grabbing check run by ancilla. Bonk state prizes also don't bother checking their own status to see if they should allow contact with an ancilla. Once the drag is performed, it can't be undone, so pre-grabbed sprites will be consumed as soon as they're freed. Or, if the ancilla slot they latched is occupied by anything, they will follow it around.