Page 1 of 1

How to add 6-button pad and SEGA Team Player support to Sonic 1

Posted: Tue Jan 17, 2017 2:10 pm
by Natsumi
Yes, you read that right: you can have 4-player multiplayer support in Sonic 1! Or not actually. But you can add support for 8 total controllers for your hack! Why? I don't know... Go do something interesting with it. All I can offer you is a tutorial on how to add it.

Now, I am well aware there has been a 6-button tutorial before, but honestly speaking, it is terrible. I wanted to add 6-button pad support for something I am working on and... It did work, but I was embarrassed sticking it into my game. Not to mention, it never actually checked if player 2 had 6-button pad, and it was just excessively slow and hacky solution.

So, I set out to write my own code. The end result? Way faster 3-button and 6-button read codes, and also full multitap support! So, why would you want to use the 6-button controller or multitap? Here are a few reasons;
  • You can use the extra X, Y, Z and MODE buttons for debugging.
  • You can use the extra X, Y, Z and MODE buttons for new content, such as moves, special stages, or minigames.
  • You can use the extra X, Y, Z and MODE buttons for a custom game or homebrew.
  • You can use the multitap for adding support for more players at once.
  • You can use the multitap for adding extra features for other people to use (for example, the second player in Super Mario Galaxy).
There are also a couple of issues you have to consider before adding the support;
  • More RAM is required. For 6-button alone, total RAM usage is 0xC bytes, and for multitap it is 0x26 bytes(!).
  • It is more time consuming to read 6-button pads.
  • It is more time consuming to read EA 4 way play (aka EXTRA mode in multitap devices).
  • Multitap protocol is the ultimate troll (Takes ~8 scanlines for 4 controllers with one Team Player inserted, as opposed to 2 scanlines for EA or singles).
  • SEGA engineers are terrible people and they probably hate you too.
  • The user can switch between EXTRA, MULTI, and A/B/C/D modes with multitap while the game is running.
  • You can poll this change, but it lowers the responsivity slightly.
Teh tutorialz
Before we get to the fun part, first we have to do something as a preparation. Since we will be using a word instead of a byte to store each button press to fit in the 6-button pad buttons too, we will have to replace every single instance of button press checking to suit this change. What you need to do is quite long-winded, and to make it easier for you, I've put them into a nice list. Remember to read it carefully first!
  • In Hivebrain 2005 disassembly;
    • Change each $FFFFF602 to P1Held
    • Change each $FFFFF603 to P1Press
    • Change each $FFFFF604 to Ctrl1Held
    • Change each $FFFFF605 to Ctrl1Press
  • In Github Sonic 1 disassembly;
    • Change each v_jpadhold2 to P1Held
    • Change each v_jpadpress2 to P1Press
    • Change each v_jpadhold1 to Ctrl1Held
    • Change each v_jpadpress1 to Ctrl1Press
  • On each line with either of the above, also do the following things (when it applies);
    • If that line starts with btst, stick a +1 right after P1Held, P1Press, Ctrl1Held or Ctrl1Press.
    • If that line contains .w at the beginning (e.g. 'move.w'), change it to .l
    • Same is true if that line contains .b at the beginning, but you change it to .w instead.
    • If the line starts with andi, and contains P1Held, P1Press, Ctrl1Held or Ctrl1Press, stick +1 right after them.
There is also a few lines we need to manually edit. Right before loc_EC70, replace this (or similar) line:

Code: Select all

        move.l    #$800,P1Held.w ; make Sonic run to    the right
with:

Code: Select all

        move.l    #(J_R)<<16,P1Held.w ; make Sonic run to    the right
And above locret_1AC60, change:

Code: Select all

        move.l    #$800,P1Held.w ; make Sonic run to    the right
to this:

Code: Select all

        move.l    #(J_R)<<16,P1Held.w ; make Sonic run to    the right
Now, one more thing to set up; demos. They still assume that we are using only a byte for our button presses. Lets fix this. Go to loc_4056, and before this line:

Code: Select all

        move.b    d1,(a0)+
just add this line:

Code: Select all

         clr.b    (a0)+
Then go to MainGameLoop, and a little above it, remove this line:

Code: Select all

        bsr.w    JoypadInit
Now, onto more interesting things. At the top of your main asm file, add the following:

Code: Select all

CTRL_ENABLE_MULTI =    1; enable multitap (Team Player) and EA 4-way play.
    rsset 0
JbU        rs.b 1    ; bit Up
JbD        rs.b 1    ; bit Down
JbL        rs.b 1    ; bit Left
JbR        rs.b 1    ; bit Right
JbB        rs.b 1    ; bit B
JbC        rs.b 1    ; bit C
JbA        rs.b 1    ; bit A
JbS        rs.b 1    ; bit Start
JbZ        rs.b 1    ; bit Z
JbY        rs.b 1    ; bit Y
JbX        rs.b 1    ; bit Z
JbM        rs.b 1    ; bit Mode

J_U =    (1<<JbU)    ; Up
J_D =    (1<<JbD)    ; Down
J_L =    (1<<JbL)    ; Left
J_R =    (1<<JbR)    ; Right
J_P =    J_U|J_D|J_L|J_R    ; UDLR
J_B =    (1<<JbB)    ; B
J_C =    (1<<JbC)    ; C
J_A =    (1<<JbA)    ; A
J_ABC =    J_A|J_B|J_C    ; ABC
J_S =    (1<<JbS)    ; Start
J_Z =    (1<<JbZ)    ; Z
J_Y =    (1<<JbY)    ; Y
J_X =    (1<<JbX)    ; X
J_XYZ =    J_X|J_Y|J_Z    ; XYZ
J_M =    (1<<JbM)    ; Mode

PollChgCTRL =    $FFFFF601    ; nonzero if we want to poll CTRL changes
P1Held =    $FFFFF602    ; held buttons for player 1
P1Press =    $FFFFF604    ; pressed buttons for player 1
P2Held =    $FFFFF606    ; held buttons for player 2
P2Press =    $FFFFF608    ; pressed buttons for player 2

    rsset $FFFFFF86
    ifne CTRL_ENABLE_MULTI
CTRL_STORE_REGS REG d0-d4/a0-a2
Ctrl1Held    rs.w 1        ; held buttons for controller 1
Ctrl1Press    rs.w 1        ; pressed buttons for controller 1
Ctrl1BHeld    rs.w 1        ; held buttons for multitap 1B
Ctrl1BPress    rs.w 1        ; pressed buttons for multitap 1B
Ctrl1CHeld    rs.w 1        ; hel┬žd buttons for multitap 1C
Ctrl1CPress    rs.w 1        ; pressed buttons for multitap 1C
Ctrl1DHeld    rs.w 1        ; held buttons for multitap 1D
Ctrl1DPress    rs.w 1        ; pressed buttons for multitap 1D
Ctrl2Held    rs.w 1        ; held buttons for controller 2
Ctrl2Press    rs.w 1        ; pressed buttons for controller 2
Ctrl2BHeld    rs.w 1        ; held buttons for multitap 2B
Ctrl2BPress    rs.w 1        ; pressed buttons for multitap 2B
Ctrl2CHeld    rs.w 1        ; held buttons for multitap 2C
Ctrl2CPress    rs.w 1        ; pressed buttons for multitap 2C
Ctrl2DHeld    rs.w 1        ; held buttons for multitap 2D
Ctrl2DPress    rs.w 1        ; pressed buttons for multitap 2D
Ctrl1State    rs.b 2        ; ctrl 1 and 2 state (0 = 3-button, $FF = 6-button, $FE - multitap, $EA = EA 4-way play)
Ctrl1MTypes    rs.b 2        ; multitap ctrl types (2 bits per ctrl, 00 = 3-btn, 01 = 6-btn, 11 = none)
    else
CTRL_STORE_REGS REG d0-d3/a0-a2
Ctrl1Held    rs.w 1        ; held buttons for controller 1
Ctrl1Press    rs.w 1        ; pressed buttons for controller 1
Ctrl2Held    rs.w 1        ; held buttons for controller 2
Ctrl2Press    rs.w 1        ; pressed buttons for controller 2
Ctrl1State    rs.b 2        ; ctrl 1 and 2 state (0 = 3-button, $FF = 6-button)
    endif

CTRLbTH =    6        ; TH pin bit
CTRLbTR =    5        ; TR pin bit
CTRLbTL =    4        ; TL pin bit
CTRL_TH =    1<<CTRLbTH    ; TH pin
CTRL_TR =    1<<CTRLbTR    ; TR pin
CTRL_TL =    1<<CTRLbTL    ; TL pin

; this instruction is basically 2 nops, except it affects cc too and is 2 bytes shorter
ctrl_delay    macro
    or.l    d0,d0
    endm
Now, we will edit the subroutines that handle button presses. Replace everything between JoypadInit and VDPSetupGame. with this:

Code: Select all

InitPads:
        moveq    #0,d2
        lea    Ctrl1State.w,a0

    ifne CTRL_ENABLE_MULTI
        lea    Ctrl1MTypes.w,a2; get multitap button list to a2
        jsr    CheckEA(pc)
    endif

        clr.w    (a0)
        lea    $A10003,a1
        bsr.s    .ctrl
        addq.w    #2,a1
    ifne CTRL_ENABLE_MULTI
        addq.w    #1,a2

.ctrl        move.b    #CTRL_TH|CTRL_TR,6(a1)
        jsr    CheckTeamPlay(pc)

        move.b    #CTRL_TH,6(a1)
        moveq    #0,d2        ; th lo
    endif

        move.b    d2,(a1)        ; Pull TH line low
        nop
        moveq    #CTRL_TH,d3    ; th hi
        move.b    d3,(a1)        ; Pull TH line high
    ctrl_delay            ; delay
        move.b    d2,(a1)        ; Pull TH line low
    ctrl_delay            ; delay
        move.b    d3,(a1)        ; Pull TH line high
    ctrl_delay            ; delay
        move.b    d2,(a1)        ; Pull TH line low
    ctrl_delay            ; delay

        move.b    (a1),d0        ; 6-BUTTON
        and.b    #$f,d0
        seq    d1        ; if 6-button, set d1
        add.b    d1,(a0)+    ; (a0) = $FF if 6-button
    rts

    ifne CTRL_ENABLE_MULTI
CheckEA:
        lea    $A10005,a1
        moveq    #CTRL_TH,d1

        move.b    d1,4(a1)    ; TH is output line on port 1
    ctrl_delay
        move.b    #$7F,6(a1)    ; all lines are output on port 2
    ctrl_delay
        move.b    #$7C,(a1)    ; get response from port 1

        moveq    #3,d0        ; all bits will be 1, but the 2 lowest ones
        and.b    -2(a1),d0    ; and the pad value
        bne.s    .notEA        ; if those bits were not 0, branch

    ; found EA 4-way play
        move.b    #$C,(a1)    ; reset latch
        addq.l    #4,sp        ; do not continue normally
        move.w    #$EAEA,(a0)    ; enable EA mode
        rts

.notEA        move.b    d1,6(a1)
        rts

CheckTeamPlay:
        move.b    #CTRL_TH|CTRL_TR,(a1)
        moveq    #CTRL_TR,d2
        moveq    #3-1,d4        ; do 3 loops later on

        moveq    #$F,d0        ; prepare and value
        and.b    (a1),d0        ; and d0 with the value we got (saves 8 cycles this way :P)
        move.b    d2,(a1)

.readdat    moveq    #$F,d1        ; prepare and value
        lsl.w    #4,d0        ; make room for new data
        jsr    MTapWaitHandShake(pc)

        and.b    (a1),d1        ; and d1 with the value we got
        move.b    d2,(a1)
        or.b    d1,d0        ; and or the new value in too
        dbf    d4,.readdat    ; loop til all ctrls are done

        cmp.w    #$3F00,d0    ; check if this is a multitap
        bne.s    .end        ; if not, branch

        move.b    #-2,(a0)+    ; set to be a multitap ctrl
        clr.b    (a2)        ; ensure we wont break this
        moveq    #0,d1        ; rol 0 times
        moveq    #4-1,d4        ; 4 ctrls

.mulloop    jsr    MTapWaitHandShake(pc)
        move.b    (a1),d0        ; get button data
        move.b    d2,(a1)

        andi.b    #3,d0        ; get 2 lowest bits
        rol.b    d1,d0        ; rotate x bits
        or.b    d0,(a2)        ; then set ctrl type

        addq.w    #2,d1
        dbf    d4,.mulloop    ; loop til all ctrls are done

        move.b    #CTRL_TR|CTRL_TH,(a1)
        addq.l    #4,sp        ; do not return normally
.end        rts
    endif

ReadJoypads:
        tst.b    PollChgCTRL.w        ; are we polling for controller changes?
        beq.s    .noinit            ; if we aren't, skip this code
        moveq    #7,d0            ; we want to run once in 8 frames
        move.b    $FFFFFE0C+3.w,d0    ; and the low byte of VBlank global timer (in Github, it is v_vbla_count+3)
        beq.w    InitPads        ; if counter&7 = 0, re-initialize controllers

.noinit        lea    $A10003,a1
        lea    Ctrl1Held.w,a0
        lea    Ctrl1State.w,a2

    ifne CTRL_ENABLE_MULTI
        cmp.w    #$EAEA,(a2)        ; special: Check if EA is active
        beq.w    ReadEA            ; if so, branch
    endif

        moveq    #CTRL_TH,d3        ; TH hi
        moveq    #0,d2            ; TH lo
        bsr.s    .ctrl3
        addq.w    #2,a1

.ctrl3    ifne CTRL_ENABLE_MULTI
        move.b    (a2)+,d0        ; check if is 3 or 6-button pad or multitap
        bmi.s     .ctrl6            ; if not 3-button pad, branch
    else
        tst.b    (a2)+            ; check if is 3 or 6-button pad
        bmi.s     .ctrl6            ; if 6, branch
    endif

        move.b    d2,(a1)            ; set TH low
        moveq    #CTRL_TL|CTRL_TR,d0    ; prepare d0
        nop                ; delay
        and.b    (a1),d0            ; and controller port data (start/A)
        move.b    d3,(a1)            ; set TH high
        lsl.b    #2,d0

        moveq    #CTRL_TL|CTRL_TR|$F,d1
        and.b    (a1),d1            ; and controller port data (B/C/Dpad)
        or.b    d1,d0            ; Fuse together into one controller bit array
        not.b    d0

        clr.b    (a0)+            ; clear high byte
        move.b    (a0),d1            ; get pressed button data
        eor.b    d0,d1            ; toggle off inputs that are being held
        move.b    d0,(a0)+        ; put held buttons to a0
        and.b    d0,d1            ; only activate buttons that were pressed this frame (but not held)

        clr.b    (a0)+            ; clear high byte
        move.b    d1,(a0)+        ; and then save pressed buttons

    ifne CTRL_ENABLE_MULTI
        clr.l    (a0)+            ; clear buttons for multitap ctrls
        clr.l    (a0)+
        clr.l    (a0)+
    endif
        rts

.ctrl6    ifne CTRL_ENABLE_MULTI
        addq.b    #1,d0            ; check if multitap
        bmi.s    .multi            ; if is, branch
    endif

        move.b    d3,(a1)            ; set TH high
        moveq    #0,d0
        moveq    #0,d1

        move.b    (a1),d1            ; Reading first 6 buttons
        move.b    d2,(a1)            ; set TH low
        andi.b    #CTRL_TL|CTRL_TR|$F,d1

        move.b    (a1),d0            ; Read second 2 buttons
        move.b    d3,(a1)            ; set TH high
        andi.b    #CTRL_TL|CTRL_TR,d0
        move.b    d2,(a1)            ; set TH low
        lsl.b    #2,d0
        move.b    d3,(a1)            ; set TH high
        or.l    d0,d1            ; Combine basic 8 buttons and store it to d1

        move.b    d2,(a1)            ; set TH low
    ctrl_delay                ; delay
        move.b    d3,(a1)            ; set TH high

        moveq    #$F,d0            ; prepare d0
        nop
        and.b    (a1),d0            ; Read extra buttons
        move.b    d3,(a1)            ; set TH high
        lsl.w    #8,d0            ; Shift it by 8 bits
        or.w    d1,d0            ; Combine it with basic buttons
        not.w    d0            ; Invert basic buttons

        move.w    (a0),d1            ; get pressed button data
        eor.w    d0,d1            ; toggle off inputs that are being held
        move.w    d0,(a0)+        ; put held buttons to a0
        and.w    d0,d1            ; only activate buttons that were pressed this frame (but not held)
        move.w    d1,(a0)+        ; and then save pressed buttons

    ifne CTRL_ENABLE_MULTI
        clr.l    (a0)+            ; clear buttons for multitap ctrls
        clr.l    (a0)+
        clr.l    (a0)+
    endif
        rts

    ifne CTRL_ENABLE_MULTI
.multi        moveq    #CTRL_TR,d2        ; TR hi
        move.b    2-1(a2),d4        ; get the status of connected ctrls

    rept 7
        move.b    d2,(a1)
        jsr    MTapWaitHandShake(pc)
    endr

        move.b    d2,(a1)
        bsr.s    .mctrldo        ; get button presses
        bsr.s    .mctrldo        ; get button presses
        bsr.s    .mctrldo        ; get button presses
        bsr.s    .mctrldo        ; get button presses

        move.b    #CTRL_TR|CTRL_TH,(a1)
        moveq    #CTRL_TH,d3        ; TH hi
        moveq    #0,d2            ; TH lo
        rts

.mctrldo    moveq    #3,d0            ; prepare and value
        and.b    d4,d0            ; and with ctrl value
        lsr.b    #2,d4            ; shift out this ctrl dat
        add.b    d0,d0            ; double d0
        jsr    .mxof(pc,d0.w)        ; jump to code

        cmp.b    #J_S|J_A|J_R|J_L,-1(a0)    ; check if this specific code is met (glitch when switching from mtap)
        bne.s    .tok            ; if is not, skip

        clr.l    -4(a0)            ; ignore user input for now
.tok        rts

.mxof        bra.s    .m3
        bra.s    .m6

        nop                ; fall through
        clr.l    (a0)+            ; clear next buttons
        rts

.m6        bsr.s    .get3
        jsr    MTapWaitHandShake(pc)
        move.b    (a1),d1            ; get SABC
        move.b    d2,(a1)

        andi.w    #$f,d1            ;
        lsl.w    #8,d1            ; to high byte
        or.w    d1,d0            ; or back
        not.w    d0            ; negate

        move.w    (a0),d1            ; get pressed button data
        eor.w    d0,d1            ; toggle off inputs that are being held
        move.w    d0,(a0)+        ; put held buttons to a0
        and.w    d0,d1            ; only activate buttons that were pressed this frame (but not held)
        move.w    d1,(a0)+        ; and then save pressed buttons
        rts

.m3        bsr.s    .get3
        not.b    d0

        clr.b    (a0)+            ; clear high byte
        move.b    (a0),d1            ; get pressed button data
        eor.b    d0,d1            ; toggle off inputs that are being held
        move.b    d0,(a0)+        ; put held buttons to a0
        and.b    d0,d1            ; only activate buttons that were pressed this frame (but not held)
        clr.b    (a0)+            ; clear high byte
        move.b    d1,(a0)+        ; and then save pressed buttons
        rts

.get3        bsr.s    MTapWaitHandShake
        move.b    (a1),d0            ; get UDLR
        move.b    d2,(a1)

        andi.w    #$F,d0
        bsr.s    MTapWaitHandShake
        move.b    (a1),d1            ; get SABC
        move.b    d2,(a1)

        lsl.b    #4,d1
        or.b    d1,d0
        rts

MTapWaitHandShake:
        moveq    #5,d3
        eor.b    #CTRL_TR,d2        ; switch TR level
        beq.s    .tl_lo

.wait        btst    #CTRLbTL,(a1)
        dbeq    d3,.wait        ; wait for 5 attempts or for TL to be high
        rts

.tl_lo        btst    #CTRLbTL,(a1)
        dbne    d3,.tl_lo        ; wait for 5 attempts or for TL to be low
        rts

ReadEA:
        lea    $A10005,a2
        moveq    #4-1,d4
        moveq    #$C,d3
        moveq    #$10,d2

.read        move.b    d3,(a2)
    ctrl_delay
        move.b    #$00,(a1)        ; lower TH

        move.w    #$FF00|CTRL_TR|CTRL_TL,d0; prepare for and (note: $FF00 is added, so that 3-button pads have high byte being 0!)
        and.b    (a1),d0            ; then and the value
        move.b    #$40,(a1)        ; raise TH
        lsl.b    #2,d0            ; shift in place

        moveq    #CTRL_TR|CTRL_TL|$F,d1    ; prepare for and
        and.b    (a1),d1            ; then and the value
        or.l    d1,d0            ; or together

    ; now check for 6-button controllers!
        move.b    #$00,(a1)        ; lower TH
    ctrl_delay
        move.b    #$40,(a1)        ; raise TH
    ctrl_delay
        move.b    #$00,(a1)        ; lower TH
        nop

        moveq    #$F,d1            ; prepare for and
        and.b    (a1),d1            ; and the next value
        bne.s    .not6            ; apparently this means no 6-button pads
        move.b    #$40,(a1)        ; raise TH
        nop

        moveq    #$F,d1            ; prepare for and
        and.b    (a1),d1            ; and the next value
        lsl.w    #8,d1            ; shift to upper byte
        and.w    #$FF,d0            ; clear out high bits
        or.w    d1,d0            ; and then or together

.not6        not.w    d0            ; Invert basic buttons
        move.b    #$40,(a1)        ; raise TH just in case

        move.w    (a0),d1            ; get pressed button data
        eor.w    d0,d1            ; toggle off inputs that are being held
        move.w    d0,(a0)+        ; put held buttons to a0
        and.w    d0,d1            ; only activate buttons that were pressed this frame (but not held)
        move.w    d1,(a0)+        ; and then save pressed buttons

        add.b    d2,d3            ; next port read
        dbf    d4,.read        ; continue onwards
        rts
    endif
And finally, as a small touch, we need to selectively enable controller polling on certain screen modes to allow the user to switch even after game boot. We will be placing this line until further notice:

Code: Select all

        st    PollChgCTRL.w    ; enable control polling
So, let's go to Sega_WaitPallet, and right above it, place the code. Next in Title_LoadText, insert our line right before this:

Code: Select all

        move.b    #0,($FFFFFE30).w ; clear lamppost counter
Finally, we must also disable this polling at some points. We will be using the following code until further notice:

Code: Select all

        clr.b    PollChgCTRL.w    ; disable control polling
Above Level_LoadPal, place our code. Next in SS_ClrNemRam, insert our line right before this:

Code: Select all

        clr.b    ($FFFFF64E).w
Near Cont_MainLoop, insert the line right above this:

Code: Select all

        bsr.w    Pal_FadeTo
Near End_LoadData, insert the line above this:

Code: Select all

        move.w    #$1E,($FFFFFE14).w
Go to loc_5862, and put the line in. At loc_478, put in the code.

Finally, open your build script (e.g. build.bat), and stick /k right after asm68k. Now build it and see if it works! If it does not, make sure you followed all the instructions correctly. If you still have issues, here are the files for the Hivebrain 2005 disassembly with the changes applied.

How to use the 6-button pads.
The 6-button pads sport a few extra buttons, X, Y, Z and M. Using these is simple enough; You can use the equates provided by me. All Jb? (where ? can be anything) are for use with bit testing, and all J_? (where ? can be anything) are for pretty much any other operation.

Here is an example where I've chained together some button checks:

Code: Select all

        btst    #JbB,Ctrl1Held+1.w        ; check if B button is held on controller 1A
        btst    #JbM,Ctrl1BPress.w        ; check if MODE button is pressed on controller 1B
        cmp.w    #J_U|J_D|J_A|J_Y|J_M,Ctrl2DHeld.w ; check if Up, Down, A, Y and MODE are held on controller 2D
        and.w    #J_ABC|J_XYZ,Ctrl2Press.w    ; check if either A, B, C, X, Y or Z are pressed on controller 2A
        or.w    #J_S,Ctrl2CHeld.w        ; force Start to be held on controller 2C
As you can see, we can do some pretty interesting things here. Of course, you can still interface with it like any normal 3-button controller, but keep in mind all the button data is in low byte. Also, you can easily check if 6-button pad is in use, if the high bit is set (bit 15). In 6-button pads, the upper 4 bits are always being set to 1

How to use the multitap.
The multitap and EA 4-way play are slightly different from normal controllers. In order to enable these multitap features, you must put 1 in the line with CTRL_ENABLE_MULTI (or put a 0 if you want to disable it).

Let's start with SEGA's multitap. Since multitap can have up to 4 controllers attached, there is 4 entries for controllers in a row (e.g. Ctrl1Held to Ctrl1DPress). These entries can be used to read controller data for all the ports of a multitap (A-D) in MULTI setting, but only the selected port in A-D setting (EXTRA setting is EA compatibility mode, see below). To check if multitap is connected to a port, check Ctrl1State & Ctrl1State+1 (for port 2). If they are -2, multitap is connected and is in MULTI mode.

To check what controllers are inserted, check value in Ctrl1MTypes & Ctrl1MTypes+1 (for port 2). Each controller is represented by 2 bits, from A to D. Here are the button values:
  • 00 - 3-button pad
  • 01 - 6-button pad
  • 10 - none/unknown
  • 11 - none
Remember that, a multitap can be connected to either port 1, port 2 or both.

How to use the EA 4-way play.
The EA 4-way play, or the EXTRA mode on multitap devices works slightly differently. Because of this, you can not know what control pads are in use (At least with my code design, if you can find a way, please share it). The information in Ctrl1MTypes is not valid. To check if 4-way play is connected, or multitap is in EXTRA mode, Ctrl1State and Ctrl1State+1 should contain $EA.

Also, it is not possible any other controllers are inserted with 4-way play or multitap in EXTRA mode. You can still read controller input just like in multitap with the MULTI mode, but you must only use 1A-1D.

Credits, etc.
  • Multitap documentation.
  • Genesis Plus GX source code.
  • RedHotSonic - Testing help
  • MarkeyJester - Getting me a multitap to do hardware tests on! (Thanks a lot!)
  • Regen - Being stupid bitch and not supporting EA 4-way play correctly.
  • SEGA - For being idiots and making multitap be inferior to EA 4-way play.