Documentation on Wii Controller Hacks

Hello! This guide is documentation of this thread: https://gbatemp.net/threads/new-classic-controller-hacks.659837/

Many thanks to people like Vague Rant, crediar, ThomasLin and anyone else that has done hacks similar to these. This would not have existed without their efforts.

This guide is currently being worked on. My apologies if there are any formatting errors. It is also currently a bit incomplete and needs more.

Things you will need:
- Dolphin Emulator on a computer (open Dolphin and select Options -> Configuration -> Interface -> Enable Debugging UI)
- PPC Assembly knowledge
- The game you want to hack
- CodeWrite (Assembler) https://github.com/TheGag96/CodeWrite

Identifying Symbols

Before booting up the game in Dolphin, right click it and go to Properties > Filesystem
Skim through the Data Partition looking for any files with the extension .elf or .map These may be unstripped binaries or function name maps, respectively; however, .map is also a pretty generic extension, so it might literally be maps in the video game sense and not the symbol maps we're looking for. If you find .elf or .map files, you can extract them (right click -> Extract file) to anywhere on your PC. Dolphin can load .elf files directly and .map files can be opened with Symbols > Load Other Map File...
(There is a list of known Wii games with debug symbols at Retro Reversing.)

Assuming that a symbol map was NOT leftover on the disc:


1. Load game you want to hack and pause. From the dolphin menu, choose Symbols -> Generate Symbols From -> Signature Database
This feature is NOT foolproof. There are many functions dolphin wont identify, and many may be incorrectly identified.

2. Using the Symbols search field on the left side of the Code tab, search for the symbol "KPADRead" or "KPADiRead"

Usually, Dolphin is unable to find the symbol KPADRead. We can use 2 other symbols to our advantage here.
3. Search for the symbol "select_1obj_continue" and click on that. Then look in the bottom left callers section. That unidentified symbol is read_kpad_dpd. Right click it and rename it to read_kpad_dpd.

Ensure to use Symbols -> Save Symbol Map throughout this process so you do not lose any progress.

4. Inside of the symbol read_kpad_dpd, look at the Callers section again. Click on one of those callers, and rename the symbol you are in to KPADRead.

After renaming the symbol to KPADRead, you should currently be looking at a section of the disassembler which should read something like this:
Code:
8016fb58    mr    r3, r31            KPADRead
8016fb5c    mr    r4, r19            KPADRead
8016fb60    bl    ->0x8016E410    --> zz_8016e410_
8016fb64    mr    r3, r31            KPADRead
8016fb68    mr    r4, r19            KPADRead
8016fb6c    bl    ->0x8016C920    --> zz_8016c920_
8016fb70    mr    r3, r31            KPADRead
8016fb74    mr    r4, r19            KPADRead
8016fb78    bl    ->0x8016DBF0    --> read_kpad_dpd
The 2 bl calls above read_kpad_dpd are 2 other relevant KPAD functions. The one in the middle should be read_kpad_acc and the one at the top should be read_kpad_stick. Go inside both of those unnamed ones and rename it to the corresponding one. Example:
Code:
8016fb58    mr    r3, r31            KPADRead
8016fb5c    mr    r4, r19            KPADRead
8016fb60    bl    ->0x8016E410    --> read_kpad_stick
8016fb64    mr    r3, r31            KPADRead
8016fb68    mr    r4, r19            KPADRead
8016fb6c    bl    ->0x8016C920    --> read_kpad_acc
8016fb70    mr    r3, r31            KPADRead
8016fb74    mr    r4, r19            KPADRead
8016fb78    bl    ->0x8016DBF0    --> read_kpad_dpd
Good work so far! You have made good progress. Next I will cover if dolphin did NOT find select_1obj_continue

If dolphin was unable to find the symbol select_1obj_continue:
1. Use the symbols search tab again and find a symbol named "WPADProbe"

2. Set a breakpoint on the very start of that symbol. Keep running in dolphin until the Callstack section in code returns something like this:

* --- [LR = XXXXXXXX] (The callstack wont return XXXXXXXX. I just put that there since the addresses for KPAD stuff is always different for each game.)

3. Once you find that call, the area will most likely be white meaning dolphin did not find the symbol. Look for the 3 bl calls to further confirm this is KPADRead. And of course, rename the symbol to KPADRead when you have confirmed.

Find where the buttons are read by KPAD
Some games (Older SDK) read buttons directly in KPADRead, while some newer SDK games read buttons in a separate function read_kpad_button.

1. Going from the three b1 calls (read_kpad_stick, read_kpad_acc, read_kpad_dpd), scroll up until you find another bl.
2. Scroll three more instructions past that bl and check what the instruction looks like.
Code:
stw    r0, 0x0068 (r31)
Or like this:
Code:
lwz    r8, 0x0060 (r21)
The exact registers may not be the same, but the instructions (stw vs. lwz) and the offsets (0x0068 vs. 0x0060) should be persistent

If you have the first: this is an older SDK game that reads buttons directly from KPADRead
Scroll up some more until you reach an instruction that looks like this:
andi. r0, r7, 0x9FFF

Congratulations! Make note of the address that is at. That is where you will be injecting buttons into the Wii Remote.

If you have the second one, this is a newer SDK game that reads buttons inside read_kpad_button

Right click on the bl instruction and select Follow Branch
Right click > Rename Symbol > read_kpad_button
Look ahead one instruction to find the following:

andi. r0, r6, 0x9FFF

Congrats! Make sure to leave note of the address this is at.

Old SDK:
Code:
; KPADRead
; r4 holds extType
; r7 holds wiimote bitfield
; r8 holds wiimote+nunchuk bitfield
; r9 holds classic bitfield

CLASSIC:
  cmpwi r4, 0x2
  bne- RETURN

    CLASSIC_HOME:
      andi. r0, r9, 0x800
      beq- CLASSIC_UP
      ori r7, r7, 0x8000    ; home

    CLASSIC_UP:
      andi. r0, r9, 0x1
      beq- CLASSIC_DOWN
      ori r7, r7, 0x8       ; up

    CLASSIC_DOWN:
      andi. r0, r9, 0x4000
      beq- CLASSIC_LEFT
      ori r7, r7, 0x4       ; down

    CLASSIC_LEFT:
      andi. r0, r9, 0x2
      beq- CLASSIC_RIGHT
      ori r7, r7, 0x1       ; left

    CLASSIC_RIGHT:
      andi. r0, r9, 0x8000
      beq- CLASSIC_A
      ori r7, r7, 0x2       ; right

    CLASSIC_A:
      andi. r0, r9, 0x10
      beq- CLASSIC_B
      ori r7, r7, 0x800     ; a

    CLASSIC_B:
      andi. r0, r9, 0x40
      beq- CLASSIC_X
      ori r7, r7, 0x400     ; b

    CLASSIC_X:
      andi. r0, r9, 0x8
      beq- CLASSIC_Y
      ori r7, r7, 0x100     ; 2

    CLASSIC_Y:
      andi. r0, r9, 0x20
      beq- CLASSIC_L
      ori r7, r7, 0x200     ; 1

    CLASSIC_L:
      andi. r0, r9, 0x2000
      beq- CLASSIC_R
      ori r7, r7, 0x800     ; a

    CLASSIC_R:
      andi. r0, r9, 0x200
      beq- CLASSIC_ZL
      ori r7, r7, 0x400     ; b

    CLASSIC_ZL:
      andi. r0, r9, 0x80
      beq- CLASSIC_ZR
      ori r7, r7, 0x800     ; a

    CLASSIC_ZR:
      andi. r0, r9, 0x4
      beq- CLASSIC_PLUS
      ori r7, r7, 0x400     ; b

    CLASSIC_PLUS:
      andi. r0, r9, 0x400
      beq- CLASSIC_MINUS
      ori r7, r7, 0x10      ; plus

    CLASSIC_MINUS:
      andi. r0, r9, 0x1000
      beq- RETURN
      ori r7, r7, 0x1000    ; minus

RETURN:
  andi. r0, r7, 0x9FFF

New SDK:
Code:
; read_kpad_button
; r4 holds extType
; r6 holds wiimote bitfield
; r7 holds wiimote+nunchuk bitfield
; r8 holds classic bitfield

CLASSIC:
  cmpwi r4, 0x2
  bne- RETURN

    CLASSIC_HOME:
      andi. r0, r8, 0x800
      beq- CLASSIC_UP
      ori r6, r6, 0x8000    ; home

    CLASSIC_UP:
      andi. r0, r8, 0x1
      beq- CLASSIC_DOWN
      ori r6, r6, 0x8       ; up

    CLASSIC_DOWN:
      andi. r0, r8, 0x4000
      beq- CLASSIC_LEFT
      ori r6, r6, 0x4       ; down

    CLASSIC_LEFT:
      andi. r0, r8, 0x2
      beq- CLASSIC_RIGHT
      ori r6, r6, 0x1       ; left

    CLASSIC_RIGHT:
      andi. r0, r8, 0x8000
      beq- CLASSIC_A
      ori r6, r6, 0x2       ; right

    CLASSIC_A:
      andi. r0, r8, 0x10
      beq- CLASSIC_B
      ori r6, r6, 0x800     ; a

    CLASSIC_B:
      andi. r0, r8, 0x40
      beq- CLASSIC_X
      ori r6, r6, 0x400     ; b

    CLASSIC_X:
      andi. r0, r8, 0x8
      beq- CLASSIC_Y
      ori r6, r6, 0x100     ; 2

    CLASSIC_Y:
      andi. r0, r8, 0x20
      beq- CLASSIC_L
      ori r6, r6, 0x200     ; 1

    CLASSIC_L:
      andi. r0, r8, 0x2000
      beq- CLASSIC_R
      ori r6, r6, 0x800     ; a

    CLASSIC_R:
      andi. r0, r8, 0x200
      beq- CLASSIC_ZL
      ori r6, r6, 0x400     ; b

    CLASSIC_ZL:
      andi. r0, r8, 0x80
      beq- CLASSIC_ZR
      ori r6, r6, 0x800     ; a

    CLASSIC_ZR:
      andi. r0, r8, 0x4
      beq- CLASSIC_PLUS
      ori r6, r6, 0x400     ; b

    CLASSIC_PLUS:
      andi. r0, r8, 0x400
      beq- CLASSIC_MINUS
      ori r6, r6, 0x10      ; plus

    CLASSIC_MINUS:
      andi. r0, r8, 0x1000
      beq- RETURN
      ori r6, r6, 0x1000    ; minus

RETURN:
  andi. r0, r6, 0x9FFF

The above example may not have the ideal button mapping for the game you're hacking; notably, that example is for a game with the vertical Wiimote layout; the D-Pad directions will be all wrong if you're doing a horizontal (NES-style) game, so you will need to edit those and whatever else needs fixing

Next, open the CodeWrite app (linked at the start of this thread) and put your assembly button injector into the ASM field, the address you identified before with the andi. instruction into the Insertion Address field, and click the right arrow at the bottom of the window to generate a Gecko Code. Copy the resulting Gecko code and add it to Tools > Cheats Manager > Gecko Codes in Dolphin

If your gecko code works, congratulations! You just made a classic controller hack!

Adding Features​

IR Pointer support:
1. Return to read_kpad_dpd, which you identified earlier
2. locate the calc_dpd_variable function; Dolphin will not have recognized this for you so it will have an autogenerated name
3. Scroll all the way to the bottom of read_kpad_dpd and look for the function call near the end, looking like this:
Code:
8016e078    mr        r3, r31            read_kpad_dpd
8016e078    extsb    r4, r30            read_kpad_dpd
8016e078    bl        ->0x8016D610    --> zz_8016d610_
- That unrecognized function is calc_dpd_variable.
  • right click > Follow Branch

  • right click > Rename Symbol > calc_dpd_variable
    • the start of this function should look pretty much, if not exactly like this:
      Code:
      8016d610    stwu    sp, -0x0020 (sp)    calc_dpd_variable
      8016d614    mflr    r0                    calc_dpd_variable
      8016d618    stw        r0, 0x0024 (sp)        calc_dpd_variable
    • that third instruction is an ideal location to insert pointer emulation; make a note of that address
4. Locate the SDK SCGetAspectRatio function
Dolphin is very good at incorrectly recognizing SCGetAspectRatio. use the symbol search to look for SCGetWpadSpeakerVolume; you will probably get 8 or more results, most of which are wrong
The first one is almost always the real SCGetAspectRatio, which Dolphin has mis-identified
for extra confidence, check if the function right above it has been identified by Dolphin as MyNandCallback and the function right below it has been identified as SCGetDisplayOffsetH; SCGetAspectRatio is usually sandwiched between those two functions which Dolphin often does correctly identify

5. Make a note of the address where SCGetAspectRatio begins
Code:
; calc_dpd_variable
  stw r0, 0x24(sp)

; player using real pointer? (gravity already valid)
  cmpwi r4, 0x0
  bne- NO_CLASSIC

; check for Classic Controller
  lbz r0, 0x5C(r3)
  cmpwi r0, 0x2
  bne- NO_CLASSIC

; magic
  bl GRAB
MAGIC:
    GETASPECT:  .int 0x00000000     ; PUT THE ADDRESS FOR SCGetAspectRatio HERE!
    ASPECT:     .float 1.3333333333
    MULTIPLIER: .float 0.015        ; pointer speed
    TIMER:      .hword 0
                .hword 0
                .hword 0
                .hword 0
    DEADZONE:   .float 0.1
    MAX_RANGE:  .float 1.0
GRAB:
  mflr r5
  stw r5, 0x0C(sp)      ; save magic pointer

; handle four separate timers for multiplayer
  slwi r7, r25, 0x1     ; player index
  addi r7, r7, TIMER-MAGIC

STICK_WAKE:
  lfs f0, 0x74(r3)
  fabs f0, f0
  lfs f1, 0x78(r3)
  fabs f1, f1
  fadd f0, f0, f1
  lfs f2, DEADZONE-MAGIC(r5)
  fcmpo cr0, f0, f2
  blt- CHECK_TIMER
  li r6, 300
  b DELTA

; check pointer sleep timer
CHECK_TIMER:
  lhax r6, r5, r7
  cmplwi r6, 0x0
  ble- NO_CLASSIC
  subi r6, r6, 0x1

DELTA:
  sthx r6, r5, r7
  li r6, 0x2
  stb r6, 0x5E(r3)
  lwz r12, GETASPECT-MAGIC(r5)  ; SCGetAspectRatio()
  mtlr r12
  blrl
  cmpwi r3, 0x1
  mr r3, r31            ; restore input pointer
  lwz r5, 0x0C(sp)      ; restore magic pointer

  lfs f2, ASPECT-MAGIC(r5)
  bne- FULLSCREEN
  fmuls f2, f2, f2       ; widescreen
FULLSCREEN:
  lfs f3, MULTIPLIER-MAGIC(r5)

; pointer X axis
  lfs f0, 0x20(r3)
  lfs f1, 0x74(r3)       ; right stick X
  fdiv f1, f1, f2        ; divide by aspect ratio
  bl RANGE_CHECK
  stfs f0, 0x20(r3)

; pointer Y axis
  lfs f0, 0x24(r3)
  lfs f1, 0x78(r3)      ; right stick Y
  fneg f1, f1
  bl RANGE_CHECK
  stfs f0, 0x24(r3)

RETURN:
  lwz r0, 0x24(sp)
  mtlr r0
  addi sp, sp, 0x20
  blr

RANGE_CHECK:
  fmadd f0, f1, f3, f0
  lfs f1, MAX_RANGE-MAGIC(r5)
  fcmpu cr0, f0, f1
  blt CHECK_NEGATIVE
  fmr f0, f1
  b IN_RANGE

    CHECK_NEGATIVE:
      fneg f1, f1
      fcmpu cr0, f0, f1
      bgt IN_RANGE
      fmr f0, f1
      
    IN_RANGE:
      blr

NO_CLASSIC:

Depending on the game, you might want different implementations here: left stick, right stick, always on, toggled with a button; the example above supports four players, uses the right stick and the pointer is disabled after a few seconds of no movement

If you're using the example pointer routine, you must copy the address where you found SCGetAspectRatio into the following line:

Code:
 GETASPECT:  .int 0x00000000     ; PUT THE ADDRESS FOR SCGetAspectRatio HERE!
for example, SCGetAspectRatio was at 80124550, so the line looks like this for me:
Code:
  GETASPECT:  .int 0x80124550     ; PUT THE ADDRESS FOR SCGetAspectRatio HERE!

5. Back in CodeWrite, insert this routine at the address you found in calc_dpd_variable; I told you to make a note of it
6. Test the code in dolphin or on a Wii. If it works, you are done!

IMPORTANT: From what i've seen, the IR assembly doesnt always work on Dolphin. If that is the case, try the code on a Real Wii.

This is still a work in progress thread, and will be updated soon for more implementations, such as:
- Tilting
- Modifying existing Wii Remote Controls or adding Nunchuk support to games
- Whatever else i forgot to add

And of course, give feedback if the thread is difficult to follow or anything 👍
 
Info for Nunchuk Errors

For fixing Nunchuk errors, usually I will load the game up in Dolphin and let it run until it reaches some kind of Nunchuk error screen. Then in the Memory tab I will switch the search type to ASCII and search for the text that shows up in the Nunchuk error (e.g. "Please connect the Nunchuk to the Wii Remote" type of thing). If it is able to locate the error text, I can then set a read breakpoint (check the Read only button, then right click where the error text is located in memory and Toggle Breakpoint) on that text and find the code where the text is loaded.

From there, you want to use the Callstack in the Code tab to trace your way backward through the code trying to find where the actual Nunchuk check occurred which caused the game to display the error. I mostly do this part via trial and error, I'll go back one or two calls and set an execute breakpoint there (click in the left column of the disassembler in the Code tab) then switch to the Nunchuk in Dolphin settings and see whether that code is still running.

What you want to find is the point where different code paths are taken depending on which controller is connected. The extension types important to us are 0 (none), 1 (Nunchuk) and 2 (Classic Controller). So eventually you should reach a point where there's code which only runs if a specific value is not equal to 1. The extension check will generally look something like cmpwi rX, 0x1, but it's important to note that checking if something equals 1 is very generic code that's used all over the place, so finding a cmpwi rX, 0x1 doesn't mean you've found the Nunchuk check.

If you suspect you have found the Nunchuk check, set a breakpoint on that instruction and try letting the game run with no extension, Nunchuk and Classic Controller. If the register that gets checked matches the values I mentioned above based on which controller you have connected, you can be reasonably sure that it is checking the extension type. If that check fails (is not equal to 1), it means the current connected controller is not a Nunchuk. At that point, you can change this check to also accept Classic Controller, usually looking something like this in assembly which you can paste into CodeWrite:
Code:
  cmpwi r0, 0x1
  beq- RETURN
  cmpwi r0, 0x2

RETURN:
Some games may use cmplwi instead. Also the register number varies
This isn't doing anything too complex: instead of just checking if the value is 1, this checks if it's 1, then checks if it's 2. That way, whatever behavior the game was going to run if the extension type was 1 (like skipping over running the Nunchuk error) will also run if the extension type is 2. A lot of the exact process for how the Nunchuk is checked for varies wildly from game to game, but the extension type values are part of the SDK, so they're something reliable to look for.


Needs some more changes and info, this is mostly copied and pasted from the original guide
 
Last edited by awesomeee,
  • Like
Reactions: MDLG01

Site & Scene News

Popular threads in this forum