Skip to content

Instantly share code, notes, and snippets.

@comex
Last active May 18, 2024 11:42
Show Gist options
  • Save comex/43bb5d916507da6bec947725e082fa08 to your computer and use it in GitHub Desktop.
Save comex/43bb5d916507da6bec947725e082fa08 to your computer and use it in GitHub Desktop.

Investigating the transference glitch in Mario Maker 2

I watched the offset blocks video and got nerd-sniped by "we don't really know why this happens". So I spent some more time on my hobby of reverse engineering this game.

TL;DR

When a block is hit, its collision type is set to 0 (disabled), and an actor is created to play the block-hit animation. Also, the block's bottom-left corner is converted to an integer grid square, rounding towards zero, and that square in the visual appearance grid (which is separate from collision) is set to empty.

When the animation is finished, the actor takes the x/y coordinates corresponding to its center and converts them to an integer grid square, again rounding towards zero. Then it iterates over the collision list for that grid square, searching for a block with a collision type of 0 (which would normally be the same block it was spawned from). When it finds such a block, it sets its collision type based on the type of actor (e.g. if the actor was spawned from a note block, it will set it to be a note block), and then stops (it won’t alter more than one block). It also sets the entry for the same square in the visual appearance grid based on the type of actor.

Why care?

My goal was mainly just to understand the existing glitch. But what I found does allow loosening the criteria for transference a little bit:

  • The video says the offset block has to be destroyed or spawn-blocked. But it can also just be a block that's currently in an animation. All of those conditions lead to the same "disabled" collision state, and indeed that's the reason why the glitch works at all.

  • The video says that the offset block has to be the most recently placed in its 2x2. But really it just needs to be more recently placed than the block it's being overwritten with.

Going through an example in too much detail

Let's go through what's happening in the first transference example in the YouTube video.

There are three block colliders, one per block. They are each located exactly where the block was placed (not rounded to the nearest square). They are never created, destroyed, or moved; only their type changes. (But actors are created and destroyed.)

Let's pretend the note block is at grid square (10, 10), since I don't know where in the level the setup was placed, and it doesn't matter.

Then, first of all, the visual appearance grid looks like this:

  • (10, 10): note block
  • (11, 11): hard block

The hard block was originally placed at (11, 10) because the coordinate is based on the center of the block.

There's also ground at (10, 11) but ground visual tiles are not stored in the same grid. (Which is why if you have an offset ground block, it still appears at its normal location.)

Separately, the block colliders are:1

  1. (10.0, 10.0), type = 0x1b (note block)
  2. (10.0, 11.0), type = 1 (ground)
  3. (10.5, 10.5), type = 0 (disabled)

The hard block at (10.5, 10.5) was disabled when the ground block spawn-blocked it.

Each grid square has a list containing all block colliders that overlap it:

  • (10, 10): collider 3, collider 1
  • (10, 11): collider 3, collider 2
  • (11, 10): collider 3
  • (11, 11): collider 3

After the note block is hit, collider 1's type is set to 0 and an actor is spawned (which visually takes the block's place). The center of that actor is (10.5, 10.5). Separately, the bottom left corner, (10.0, 10.0), is rounded down to integer (10, 10), and that spot in the visual appearance grid is set to empty.

When the actor is done animating, it rounds down the floating-point coordinates (10.5, 10.5) to integer (10, 10), and searches the corresponding collider list. This is the correct grid square, and the correct collider (collider 1) is in the list, but the actor sees collider 3 first. Collider 3 is also in a disabled state, so the actor decides that that must be the block that spawned it. It changes collider 3's type to 0x1b (note block) and stops iterating. In addition, the actor sets the visual appearance for (10, 10) to be a note block.

So now the visual appearance is the same as before, but we have these colliders:

  1. (10.0, 10.0), type = 0 (disabled)
  2. (10.0, 11.0), type = 1 (ground)
  3. (10.5, 10.5), type = 0x1b (note block)

Now Mario hits the offset block. This sets collider 3's type back to 0, and creates an actor centered at (11.0, 11.0). Separately, the bottom left corner, (10.5, 10.5), is rounded down to integer (10, 10), and that spot in the visual appearance grid is set back to empty.

When the actor is done animating, it rounds down the floating-point coordinates (11.0, 11.0) to integer (11, 11), and searches the corresponding collider list. This is a different grid square. That doesn't actually matter for the colliders in this particular example; collider 3 is in this list too, so the actor again targets it and sets its type back to 0x1b. However, (11, 11) is also used as the coordinate in the visual appearance grid to set back to a note block, overwriting the hard block that was there before.

Yes, the visual change is performed on the bottom left corner when blocks disappear, but on the center when they reappear. Jank.

Pointers?

If anyone wants to know the specific structures and code addresses involved, I'm happy to supply them, though expect messiness. I really should publish them here, but right now I'm feeling lazy after spending my whole weekend on this :)

I investigated this with the help of some Python scripts I wrote that make it... somewhat easier to read and write some of this information from the game's memory. The scripts are extremely undocumented, but I'm happy to explain how to use them if anyone cares. But you should know how to use GDB and Python already, and know how to get a GDB stub set up on either console or Yuzu. The Python scripts are also not terribly sophisticated or easy-to-use, so don't expect much.

Footnotes

  1. In reality, all the floating-point coordinates are stored in units of 1/16th of a block, so e.g. the first collider would really be at the coordinates (160.0, 160.0), not (10.0, 10.0). I used block coordinates for simplicity. The units matter for some other glitches (floating-point rounding errors), but not for this glitch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment