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: move.l #$800,P1Held.w ; make Sonic run to the right with: Code: move.l #(J_R)<<16,P1Held.w ; make Sonic run to the right And above locret_1AC60, change: Code: move.l #$800,P1Held.w ; make Sonic run to the right to this: Code: 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: move.b d1,(a0)+ just add this line: Code: clr.b (a0)+ Then go to MainGameLoop, and a little above it, remove this line: Code: bsr.w JoypadInit Now, onto more interesting things. At the top of your main asm file, add the following: Code: 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: 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 endif .ctrl ifne CTRL_ENABLE_MULTI move.b #CTRL_TH|CTRL_TR,6(a1) jsr CheckTeamPlay(pc) beq.w .end move.b #CTRL_TH,6(a1) moveq #0,d2 ; th lo nop else move.b #CTRL_TH,6(a1) ctrl_delay 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 move.b d3,(a1) ; Pull TH line high 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 #$F,d0 ; all bits will be 1, but the 2 lowest ones (broken?) 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) moveq #0,d0 ; for beq .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 and.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: 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: 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: clr.b PollChgCTRL.w ; disable control polling Right below Level_LoadPal, place our code. Next in SS_ClrNemRam, insert our line right before this: Code: clr.b ($FFFFF64E).w Near Cont_MainLoop, insert the line right above this: Code: bsr.w Pal_FadeTo Near End_LoadData, insert the line above this: Code: 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: 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.
Oh thank goodness, I don't have to clean up and release my multitap code. Yours is way easier to migrate a hack to.
Shouldn't that instruction to disable polling be after Level_LoadPal rather than before it? Having polling enabled during gameplay seems to cause erroneous presses of X,Y,Z and Mode while having the dpad held on the frames where it polls.
Well that's stupid and awkward oversight... I did mean below it. However, it should not cause any glitches with what buttons are active, I do not know what could cause that. EDIT: Dicks, I can't seem to edit the post without the tabs breaking... Sigh...
I can reproduce the wrong inputs problem on a clean disassembly with your example sonic1.asm by just changing the PauseGame routine to check for "mode" instead of "start", and holding right in game for a few seconds (with polling still enabled). Right is on the same bit in the low byte as mode is in the high byte so I'm guessing that's something to do with the problem, but since I don't know much about how all the "pull TH high/low" stuff works I can't really see why it's doing it. A few other things i noticed that don't seem right: - doesn't assemble when the multitap code is disabled cos the ".ctrl" label is inside the disabled code - polling only occurs every 256 frames rather than every 8 like the comments suggest - the rts at the end of the InitPads routine seems to prevent it from returning to the ReadJoypads routine so the cotroller inputs aren't updated on that frame
OK, so I was able to find many of these bugs, and fix most of them... The first problem which would mess up 6-button controller input whenever polled, was because of me forgetting to pull TH high after we were done with detection. Few lines above CheckEA, there is move.b (a1),d0, add below this; move.b d3,(a1) Next, the issue with polling was my derp. As the comments suggests, move.b $FFFFFE0C+3.w,d0 was supposed to be and.b $FFFFFE0C+3.w,d0. It is completely intentional that you can not read ctrl input during the frames the controllers are polled. This is because you can not get 100% accurate output from all devices until the next frame (ish, since the reset actually occurs in less time, but not enough to do it during v-int). Also the used CPU time is a concern, because detection takes a fair bit of time. I think ignoring input every 8 frames is good enough compromise. Finally, there seems to be an issue with the EA 4-way play protocol, if you hold Up and Down on the dpad at the same time, you can't really tell whether it was a controller or the multitap sending the signal. I was able to check UDLR instead, however there is no way for me to avoid this bug further that I could find. As hotfix you could check during your game's code if UDLR was pressed or held, and ignore any button input if done so. However, I do not consider this major enough to dive deep into the protocol to fix this fully in every case, because you really cant hold all the directions at once in actual hardware. Alas, this will be an issue you have to deal with if you so choose. few lines into CheckEA, you will see moveq #3,d0, just change it to moveq #$F,d0. I am not exactly sure why this works however (as the comment suggests, this should NOT work). Unfortunately EA 4-way play is not really documented anywhere, so I had hard time checking if the code works right. Since I can't seem to edit the posts still, these changes will not yet be reflected in the actual tutorial, however I have updated the download with the changes intact.
Why can't you edit your first post? Is it something I missed? Also, good work on getting 4 controllers working. Now if only I could be arsed (and had the time) to continue working on Sonic Bash =P
The post has been edited to reflect these changes, finally. It looks like disabling using rich text boxes fixed the issue, though its rather silly thing to have to do...