Responding to Controller Inputs

in Super Mario Odyssey

As we continue to learn more about Super Mario Odyssey’s internals, increasingly complex mods have slowly become possible — but there’s still a lot of basic functionality missing. One fairly core component in that area is the ability to perform actions based on controller input.

This post aims to document SMO’s controller system, with the goal of acting as a starting point for any mod that requires interaction with the controller.

What to Hook

For the basic case of performing an action based on a button press, we need to find a portion of the game’s code that we can modify to actually perform the check.

The most appropriate place to do this might vary depending on the mod — but for many cases, StageScene::control (1.0.0: 004CC348, 1.3.0: 00472AF0) is a good contender. This function is called on every frame where the player has control of the game (including during most cutscenes and when the game is paused, since both of those scenarios also need to listen for controller inputs).

We’ll look at actually implementing this hook further down, but first, we need to establish how to actually read button states.

Button Reference Table

To query a button’s state (as discussed below), you’ll need to know either its name or its numeric value.

Here is a list of every button used within the game. There’s actually logic for some additional buttons too, including buttons named 1, 2, Start, and Select — but these are never read within the game.

Button NameNumeric Valueal::isPadTrigger (1.0.0)al::isPadTrigger (1.3.0)Description
A00085C2E8005CFBD0
B10085C380005CFC80
ZL20085C548005CFE90
X30085C418005CFD30
Y40085C4B0005CFDE0
ZR50085C5E0005CFF40
PressRightStick60085DD18005D09A0Pressing in on Right Control Stick
PressLeftStick70085DC80(None)Pressing in on Left Control Stick
Minus90085D26C005D04C0
Plus100085D1D4005D0410
L130085C678005CFFF0
R140085C710005D00A0
Up160085C8D8005D0150Up on D-Pad
Down170085C970005D0200Down on D-Pad
Left180085CA08005D02B0Left on D-Pad
Right190085CAA0005D0360Right on D-Pad

Getting the Controller Port

All functions that return button states accept a controller port number as an argument — though most of the time, you will not need to get this number yourself.

Calling al::getMainControllerPort(void) (1.0.0: 0085BFB0, 1.3.0: 005CF910) will return the port number for Player 1’s controller; alternatively, al::getPlayerControllerPort(int) (1.0.0: 0085BFEC, 1.3.0: 005CF950) can be used to get the controller port for either Player 1 or Player 2.

In general, you can simply pass -1 as the controller port value where needed, and Player 1’s controller port number will be automatically obtained using an inlined copy of al::getMainControllerPort(void). Manually getting a controller port number is only helpful if accessing Player 2’s controller, or for better performance when checking multiple button values sequentially.

What to Call

With all that background established, we can now dive into how to actually check button values. There are a couple of ways to do this; we’ll start with the easiest and then move on to most generic.

Predefined Button Functions

SMO contains a whole suite of functions that can be used to check the state of an individual button on the controller.

For example, if we want to check if the Left D-Pad button is pressed (a good choice for mods, since it’s mapped to nothing in-game), we can call al::isPadTriggerLeft(int), passing in our controller’s port number (or -1 for the default). This function will return 1 if the Left D-Pad button is currently pressed, and 0 if not.

There are four sets of functions available here:

  • al::isPadTrigger{BUTTON_NAME} (button just became pressed)
  • al::isPadHold{BUTTON_NAME} (button is currently pressed)
  • al::isPadRelease{BUTTON_NAME} (button just became released)
  • al::isPadRepeat{BUTTON_NAME} (unclear; despite the name, seems to behave the same as Hold, and is unused throughout the game)

These functions are the easiest way to check controller input values. You can find the addresses for the Trigger functions in the table above.

There are a couple of reasons why you may need to drop to a lower level though:

  1. Functions that are unreferenced in the game, while available in 1.0.0, are stripped out in 1.3.0. This shouldn’t impact most of the basic Trigger functions you might want to use, but does eliminate al::isPadTriggerPressLeftStick (hence the missing value in the table above), along with making some Hold/Release functions, plus every Repeat function, unavailable.
  2. If you’re dynamically determining which button to check, the lower-level options will be more convenient.

Arbitrary Button Functions

In addition to the button-specific functions above, there are two general functions that allow for checking the state of a button given its numeric value:

  • al::isPadTrigger(int, int)
  • al::isPadHold(int, int)

Like the previous functions, these two also take a controller port as their first parameter, and their second parameter indicates which button to read the value for (e.g., al::isPadTrigger(-1, 5) would return a value indicating whether the ZR button is pressed on the default controller).

As mentioned above, these are particularly useful if you need to check the state of a button whose direct function does not exist on 1.3.0 — or if you simply have a use case for determining the button to check at runtime.

Full Example

Here’s an annotated example of an implementation that sets Mario’s Y coordinate to 0 when the Left D-Pad button is pressed (the effective result of which is either teleporting Mario upwards, or forcing him into the ground if he is currently in the air, depending on your location within the level)

We can start by replacing an instruction near the beginning of StageScene::control — specifically, just after the function prologue — with a branch to another bit of code that we control.

Note: I usually start my custom code at 0085E000 for 1.0.0 and 007AEB40 for 1.3.0. I believe the latter case breaks VR code, which is a fine tradeoff for me. We’ll use 1.0.0 in the examples below.

; Branch from address 004CC360 (in `StageScene::control`) to address 0085E000
BL     0x391CA0 ; .pchtxt: 004CC360 28470E94

Now we can define our custom code starting at address 0085E000. This implementation will include a basic function prologue (we need to store X30 so that we can later return to our original caller), a replacement for our hooked instruction, our actual button detection / action logic, and then some state restoration, a function epilogue, and finally a return instruction.

; Store X29 & X30
STP    X29, X30, [SP, -0x10]! ; .pchtxt: 0085E000 FD7BBFA9

; Re-implement the instruction we replaced in `StageScene::control`
MOV    X19, X0                ; .pchtxt: 0085E004 F30300AA

; Call al::isPadTriggerLeft,
; passing -1 as the controller port number
; (meaning we'll end up using the default controller port)
MOV     W0, -1                ; .pchtxt: 0085E008 00008012
BL      al::isPadTriggerLeft  ; .pchtxt: 0085E00C 7FFAFF97

; Check the returned result.
; If the button is not pressed (i.e., W0 == 0),
; skip ahead to state restoration / function epilogue / return
CBZ     W0, 0x14              ; .pchtxt: 0085E010 A0000034

; Call `rs::getPlayerActor`, which takes in a Scene object
; (which happens to be in X19 based on our hook in `StageScene::control`)
; and returns the current player actor.
MOV     X0, X19               ; .pchtxt: 0085E014 E00313AA
BL      rs::getPlayerActor    ; .pchtxt: 0085E018 4CD3F197

; Set the player actor's Y coordinate to 0
FMOV    S0, 0                 ; .pchtxt: 0085E01C E003271E
BL      al::setTransY         ; .pchtxt: 0085E020 4B430294

; Restore X0 so we're in the same state as we were when we started
; (since our button-checking logic will end up modifying X0)
MOV    X0, X19                ; .pchtxt: 0085E024 E00313AA

; Restore X29 & X30
LDP    X29, X30, [SP], 0x10   ; .pchtxt: 0085E028 FD7BC1A8

; Return
RET                           ; .pchtxt: 0085E02C C0035FD6

Combining these replacements gives us a single patch that properly responds to button inputs in an SMO mod. You can download the resulting .pchtxt files here:

1.0.0 .pchtxt
1.3.0 .pchtxt

While this particular implementation isn’t too exciting, the techniques covered here should allow us to finally start to build mods that integrate control inputs, and should open the door to some more interesting & complex mods in the near future.