Good news! Another level tutorial for VS mode! Bad news! It's lengthy and pedantic as hell! So yeah, it's Oil Ocean Zone this time round, and befitting the zone itself, this is definitely the 'hard mode' of the level tutorials, so you'll have to sit with me for this one as it might take me a while to note down everything. Download sample ROM below. (NOTE: This tutorial is for the Xenowhirl edit. Most of this is likely compatible with the GitHub version but will need a bit of relabelling.) Like the last tutorials, I'm going to do this in sequences so I get this right one by one. This might take a while so please hold on replies until the tutorial is fully posted so it's all in one piece by the end of it. Step 1: Making it available in the VS menu: First we need to go to MenuScreen and change what one of the icons in the 2P menu directs to. To shake it up we'll edit Casino Night Zone this time. Go to the directs in word_8E52 and change the second entry to A (Oil Ocean's placement in the game's code): Code (Text): word_8E52: dc.w 0 ; 0 (EMERALD HILL) dc.w $B00 ; 1 (MYSTIC CAVE) dc.w $A00 ; 2 (OIL OCEAN) dc.w $FFFF ; 3 (SPECIAL STAGE) Now we need to edit the icon to match it. The data for the level image is in off_8F7E. Again to exemplify we'll edit the Casino Night icon, so it will be the third three entries, editing the art and palette. Code (Text): off_8F7E: dc.l byte_874A ;"EMERALD HILL" dc.l byte_878C ;"ZONE " dc.w $4104 dc.w 3 dc.w $FF ;'EHZ Icon Palette Line' dc.w $330 ;'EHZ Icon Art tiles' dc.l byte_8757 ;" MYSTIC CAVE" dc.l byte_878C ;"ZONE " dc.w $412C dc.w 3 dc.w $5FF ;'MCZ Icon Palette line' dc.w $3A8 ;'MCZ Icon Art tiles' dc.l byte_8764 ;" OIL OCEAN " dc.l byte_878C ;"ZONE " dc.w $4784 dc.w 3 dc.w $4FF ;'OOZ Icon Palette line' dc.w $390 ;'OOZ Icon Art tiles' dc.l byte_877F ;" SPECIAL " dc.l byte_8792 ;"STAGE" dc.w $47AC dc.w 3 dc.w $CFF ;'SS Icon Palette line' dc.w $450 ;'SS Icon Art Tiles' Finally go to word_8732 and then find the entries below reading the 2 player text, and edit accordingly. Remember when altering the header to keep it the same number characters listed before it. Code (Text): ; 2-player mode menu text byte_874A: dc.b $B,"EMERALD HILL" byte_8757: dc.b $B," MYSTIC CAVE" byte_8764: dc.b $B," OIL OCEAN " byte_8771: dc.b $C,"SPECIAL STAGE" byte_877F: dc.b $B," SPECIAL " byte_878C: dc.b 4,"ZONE " byte_8792: dc.b 4,"STAGE" byte_8798: dc.b 8,"GAME OVER" dc.b 8,"TIME OVER" byte_87AC: dc.b 6,"NO GAME" byte_87B4: dc.b 3,"TIED" byte_87B9: dc.b 2," 1P" byte_87BD: dc.b 2," 2P" byte_87C1: dc.b 3," " charset ; reset character set And viola, Oil Ocean Zone is now available in place of Casino Night. Step Two coming soon.
Step 2: Making Split screen compatible level art. And we enter the level, and, just like Chemical Plant, it looks like crap unoptimised. To reiterate from the Chemical Plant tutorial in case you didn't read that one, in order to expand the resolution, the split screen interlace loads tiles as 8 by 16 rather than 8 by 8, interchanging between odd and even scanlines of each tile frame by frame. This means it needs all pixel art to be loaded as tiles of 8 by 16 pixels, both in level art chunks, and sprites for objects and characters, otherwise it will mix tiles up and the art will often display as an incoherent mess. To fix this we need to essentially do the same as with the character and sprite fix tutorial, however we are limited to adjacent blocks in level editing like so. The top tile must be an even VRAM ID and the tile below must come right after. The tiles besides them don't need to follow right after but must follow the same rules themselves. If the block itself is flipped don't worry, the display can usually handle that, but it's default state must follow this rule. This also means that the start of the art file needs at least TWO blank tiles to make the empty opening block display properly, again following the same rule. Doing such is a tricky process, since this will obviously take up way more VRAM than the normal art file, meaning some sacrifices in detail will need to be made. Like before, I have left some sample level files to make things a bit easier, as before they are owed greatly to the optimization work of Sock Team, though I've retooled these ones to use less tiles and follow the original artwork where possible. There's again some rough spots, though mostly where tile work can be repeated. I'm gonna leave a patch work file later in the tutorial this time so you can mess around with it. Step Three coming soon.
Step Three: Making separate art load for VS mode: Well now we have art to load for VS mode but we aren't able to load it. For that we will once again go to Devon's tutorial for segregating level asset loads. If you already used this you can just make the neccessary edits for Oil Ocean and skip this part. https://forums.sonicretro.org/index...ique-level-data-per-act-and-in-2p-mode.39478/ First, take the OOZ VS level files and add them to your disassembly. Now add your newly renamed 2P mapping folders into the directory: Code (Text): ;----------------------------------------------------------------------------------- ; OOZ 16x16 block mappings (Kosinski compression) BM16_OOZ: BINCLUDE "mappings/16x16/OOZ.bin" ;----------------------------------------------------------------------------------- ; OOZ main level patterns (Kosinski compression) ; ArtKoz_A4204: ArtKos_OOZ: BINCLUDE "art/kosinski/OOZ.bin" ;----------------------------------------------------------------------------------- ; OOZ 128x128 block mappings (Kosinski compression) BM128_OOZ: BINCLUDE "mappings/128x128/OOZ.bin" ;----------------------------------------------------------------------------------- ; OOZ 16x16 block mappings (Kosinski compression) BM16_OOZ_2P: BINCLUDE "mappings/16x16/OOZ_2P_Block.bin" ;----------------------------------------------------------------------------------- ; OOZ main level patterns (Kosinski compression) ; ArtKoz_A4204: ArtKos_OOZ_2P: BINCLUDE "art/kosinski/OOZ_2P_Art.bin" ;----------------------------------------------------------------------------------- ; OOZ 128x128 block mappings (Kosinski compression) BM128_OOZ_2P: BINCLUDE "mappings/128x128/OOZ_2P_Chunk.bin" Now we're gonna go to LevelArtPointers. What we're going to do is duplicate every level so that the rest of the asm will recognise 2 player counterparts as separate (this will handy if you want to edit any other two player levels). Edit your new 2 player directory for Chemical Plant to have the new 2P mappings like so: Code (Text): ; BEGIN SArt_Ptrs Art_Ptrs_Array[17] ; dword_42594: MainLoadBlocks: saArtPtrs: LevelArtPointers: levartptrs 4, 5, 4, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 0 ; EHZ ; EMERALD HILL ZONE levartptrs 4, 5, 4, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 0 ; EHZ ; EMERALD HILL ZONE (2 PLAYER) levartptrs 6, 7, 5, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 1 ; LEV1 ; LEVEL 1 (UNUSED) levartptrs 6, 7, 5, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 1 ; LEV1 ; LEVEL 1 (UNUSED) (2 PLAYER) levartptrs 8, 9, 6, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 2 ; LEV2 ; LEVEL 2 (UNUSED) levartptrs 8, 9, 6, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 2 ; LEV2 ; LEVEL 2 (UNUSED) (2 PLAYER) levartptrs $A, $B, 7, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 3 ; LEV3 ; LEVEL 3 (UNUSED) levartptrs $A, $B, 7, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 3 ; LEV3 ; LEVEL 3 (UNUSED) (2 PLAYER) levartptrs $C, $D, 8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ; 4 ; MTZ ; METROPOLIS ZONE ACTS 1 & 2 levartptrs $C, $D, 8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ; 4 ; MTZ ; METROPOLIS ZONE ACTS 1 & 2 (2 PLAYER) levartptrs $C, $D, 8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ; 5 ; MTZ3 ; METROPOLIS ZONE ACT 3 levartptrs $C, $D, 8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ; 5 ; MTZ3 ; METROPOLIS ZONE ACT 3 (2 PLAYER) levartptrs $10,$11, $A, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ; 6 ; WFZ ; WING FORTRESS ZONE levartptrs $10,$11, $A, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ; 6 ; WFZ ; WING FORTRESS ZONE (2 PLAYER) levartptrs $12,$13, $B, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 7 ; HTZ ; HILL TOP ZONE levartptrs $12,$13, $B, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 7 ; HTZ ; HILL TOP ZONE (2 PLAYER) levartptrs $14,$15, $C, BM16_OOZ, BM16_OOZ, BM16_OOZ ; 8 ; HPZ ; HIDDEN PALACE ZONE (UNUSED) levartptrs $14,$15, $C, BM16_OOZ, BM16_OOZ, BM16_OOZ ; 8 ; HPZ ; HIDDEN PALACE ZONE (UNUSED) (2 PLAYER) levartptrs $16,$17, $D, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 9 ; LEV9 ; LEVEL 9 (UNUSED) levartptrs $16,$17, $D, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 9 ; LEV9 ; LEVEL 9 (UNUSED) (2 PLAYER) levartptrs $18,$19, $E, ArtKos_OOZ, BM16_OOZ, BM128_OOZ ; $A ; OOZ ; OIL OCEAN ZONE levartptrs $18,$19, $E, ArtKos_OOZ_2P, BM16_OOZ_2P, BM128_OOZ_2P ; $A ; OOZ ; OIL OCEAN ZONE (2 PLAYER) levartptrs $1A,$1B, $F, ArtKos_MCZ, BM16_MCZ, BM128_MCZ ; $B ; MCZ ; MYSTIC CAVE ZONE levartptrs $1A,$1B, $F, ArtKos_MCZ, BM16_MCZ, BM128_MCZ ; $B ; MCZ ; MYSTIC CAVE ZONE (2 PLAYER) levartptrs $1C,$1D,$10, ArtKos_CNZ, BM16_CNZ, BM128_CNZ ; $C ; CNZ ; CASINO NIGHT ZONE levartptrs $1C,$1D,$10, ArtKos_CNZ, BM16_CNZ, BM128_CNZ ; $C ; CNZ ; CASINO NIGHT ZONE (2 PLAYER) levartptrs $1E,$1F,$11, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ; $D ; CPZ ; CHEMICAL PLANT ZONE levartptrs $1E,$1F,$11, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ; $D ; CPZ ; CHEMICAL PLANT ZONE (2 PLAYER) levartptrs $20,$21,$12, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ; $E ; DEZ ; DEATH EGG ZONE levartptrs $20,$21,$12, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ; $E ; DEZ ; DEATH EGG ZONE (2 PLAYER) levartptrs $22,$23,$13, ArtKos_ARZ, BM16_ARZ, BM128_ARZ ; $F ; ARZ ; AQUATIC RUIN ZONE levartptrs $22,$23,$13, ArtKos_ARZ, BM16_ARZ, BM128_ARZ ; $F ; ARZ ; AQUATIC RUIN ZONE (2 PLAYER) levartptrs $24,$25,$14, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ; $10 ; SCZ ; SKY CHASE ZONE levartptrs $24,$25,$14, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ; $10 ; SCZ ; SKY CHASE ZONE (2 PLAYER) Then replace the levartptrs macro above it like so: Code (Text): ; declare some global variables to be used by the levartptrs macro cur_zone_id := 0 cur_zone_str := "0" cur_zone_2p := 0 ; macro for declaring a "main level load block" (MLLB) levartptrs macro plc1,plc2,palette,art,map16x16,map128x128 ; !org LevelArtPointers+zone_id_{cur_zone_str}*24+cur_zone_2p dc.l (plc1<<24)|art dc.l (plc2<<24)|map16x16 dc.l (palette<<24)|map128x128 cur_zone_2p := cur_zone_2p+12 if cur_zone_2p>=24 cur_zone_2p := 0 cur_zone_id := cur_zone_id+1 cur_zone_str := "\{cur_zone_id}" endif endm And have this added after the LevelArtPointers table: Code (Text): if (cur_zone_2p<>0)&&(MOMPASS=1) message "Warning: Table LevelArtPointers's last entry does not have a 2P entry" endif Then, go to Level and go to the comment that says "; multiply d0 by 12, the size of a level art load block". Insert this before "lea (LevelArtPointers).l,a2": Code (Text): add.w d0,d0 tst.w (Two_player_mode).w beq.s .not_2p_mode addi.w #12,d0 .not_2p_mode: Then go to both sub_4E98 and loadZoneBlockMaps and add the same code before both instances of "lea (LevelArtPointers).l,a2". With that, you should now be able to set up level data pointers for 2P mode for any zone. Step Four coming soon.
Step Four: Refining issues with the VS layout Oil Ocean is a bit more nitty gritty with refinement (yes, EVEN MORE so than Chemical Plant) since to get it looking right we actually had to edit other factors like the palette and chunk layout. 1. Palette: We'll start with the palette, add the file below into the palette folder and then add it to the palette listing in the asm. Code (Text): Pal_UNK6: BINCLUDE "art/palettes/Special Stage 3 2p.bin" ; Special Stage 3 2p palette Pal_UNK7: BINCLUDE "art/palettes/Special Stage Results Screen.bin" ; Special Stage Results Screen palette Pal_OOZ_2P: BINCLUDE "art/palettes/OOZ_2P_Palette.bin" ; Oil Ocean Zone 2p palette Then apply this to the Pal_Pointers and LevelArtPointers routines accordingly: Code (Text): PalPointers: palptr Pal_SEGA, Normal_palette, $1F palptr Pal_Title, Normal_palette_line2, 7 palptr Pal_UNK1, Normal_palette, $1F palptr Pal_BGND, Normal_palette, $F palptr Pal_EHZ, Normal_palette_line2, $17 palptr Pal_EHZ, Normal_palette_line2, $17 ...... palptr Pal_Menu, Normal_palette, $1F palptr Pal_UNK7, Normal_palette, $1F palptr Pal_OOZ_2P, Normal_palette_line2, $17 ;$28 Code (Text): levartptrs $18,$19, $E, ArtKos_OOZ, BM16_OOZ, BM128_OOZ ; $A ; OOZ ; OIL OCEAN ZONE levartptrs $18,$19, $28, ArtKos_OOZ_2P, BM16_OOZ_2P, BM128_OOZ_2P ; $A ; OOZ ; OIL OCEAN ZONE (2 PLAYER) Oil Ocean won't be using a cycling palette in VS so we'll have the game skip over PalCycle_OOZ entirely in VS mode: Code (Text): PalCycle_OOZ: tst.w (Two_player_mode).w bne.w No_PalCycle_OOZ subq.w #1,($FFFFF634).w bpl.s + ; rts move.w #7,($FFFFF634).w lea (word_1F76).l,a0 move.w ($FFFFF632).w,d0 addq.w #2,($FFFFF632).w andi.w #6,($FFFFF632).w lea (Normal_palette_line3+$14).w,a1 move.l (a0,d0.w),(a1)+ move.l 4(a0,d0.w),(a1) No_PalCycle_OOZ: + rts ; =========================================================================== 2. Chunk layout For this, take the files from the layout folder in the zip file below and add them in the same labelled folder in your disassembly. Now add them to your asm where the others are listed. Code (Text): ........ ;--------------------------------------------------------------------------------------- ; ARZ act 2 level layout (Kosinski compression) Level_ARZ2: BINCLUDE "level/layout/ARZ_2.bin" ;--------------------------------------------------------------------------------------- ; SCZ level layout (Kosinski compression) Level_SCZ: BINCLUDE "level/layout/SCZ.bin" ;--------------------------------------------------------------------------------------- ; OOZ act 1 level layout (Kosinski compression) Level_OOZ1_2P: BINCLUDE "level/layout/OOZ_1_VS.bin" ;--------------------------------------------------------------------------------------- ; OOZ act 2 level layout (Kosinski compression) Level_OOZ2_2P: BINCLUDE "level/layout/OOZ_2_VS.bin" Now we're gonna make a branch to skip over the layout routine for Oil Ocean in 2 player mode. This is a kinda half assed method, but it's pretty much the same way the game makes exceptions for Casino Night's changes in VS. Code (Text): loadLevelLayout: moveq #0,d0 move.w (Current_ZoneAndAct).w,d0 ror.b #1,d0 lsr.w #6,d0 lea (Off_Level).l,a0 move.w (a0,d0.w),d0 lea (a0,d0.l),a0 tst.w (Two_player_mode).w ; skip if not in 2-player vs mode beq.s load_notOOZ cmpi.b #$A,(Current_Zone).w ; skip if not Oil Ocean Zone bne.s load_notOOZ lea (Level_OOZ1_2P).l,a0 ; OOZ 1 2-player object layout tst.b (Current_Act).w ; skip if not past act 1 beq.s load_notOOZ lea (Level_OOZ2_2P).l,a0 ; OOZ 2 2-player object layout load_notOOZ: lea (Level_Layout).w,a1 bra.w JmpTo_KosDec ; End of function loadLevelLayout Step Five coming soon.
Step Five: Making the level scroll properly in split screen By this point when entering the level, you should have something that looks halfway presentable in Sonic's screen, but Tails' is still a glitched mess and none of the objects and animated elements are refined yet and likely overlap with the level art a lot. We'll start by making a proper splitscreen routine for the level. To cram everything into this form, the background took the biggest blow out of everything, now a very simplified 4 by 2 background with no vertical scrolling so as to allow as much VRAM space as possible for level art. As such we can basically just give Oil Ocean a duplicate of Hill Top's own simplified split screen code. First add a branch for it. Code (Text): ; loc_CC66: SwScrl_OOZ: tst.w (Two_player_mode).w bne.w OOZ_Splitscreen move.w ($FFFFEEB0).w,d0 ext.l d0 asl.l #5,d0 add.l d0,($FFFFEE08).w move.w ($FFFFEEB2).w,d0 ...... Then just before SwScrl_MCZ add this routine: Code (Text): ; =========================================================================== OOZ_Splitscreen: move.w ($FFFFEEB0).w,d4 ext.l d4 asl.l #5,d4 move.w ($FFFFEEB2).w,d5 ext.l d5 asl.l #2,d5 moveq #0,d5 bsr.w sub_D89A move.b #0,($FFFFEE52).w move.w ($FFFFEE0C).w,($FFFFF618).w andi.l #$FFFEFFFE,(Vscroll_Factor).w lea (Horiz_Scroll_Buf).w,a1 move.w #bytesToLcnt($1C0),d1 move.w (Camera_X_pos).w,d0 neg.w d0 swap d0 move.w ($FFFFEE08).w,d0 neg.w d0 - move.l d0,(a1)+ dbf d1,- move.w ($FFFFEEB8).w,d4 ext.l d4 asl.l #5,d4 add.l d4,($FFFFEE28).w moveq #0,d0 move.w d0,($FFFFF620).w subi.w #$E0,($FFFFF620).w move.w ($FFFFEE24).w,($FFFFF61E).w subi.w #$E0,($FFFFF61E).w andi.l #$FFFEFFFE,($FFFFF61E).w lea ($FFFFE1B0).w,a1 move.w #bytesToLcnt($1D0),d1 move.w ($FFFFEE20).w,d0 neg.w d0 swap d0 move.w ($FFFFEE28).w,d0 neg.w d0 - move.l d0,(a1)+ dbf d1,- rts For the camera routine, we can just straight up branch it to HTZ's, which has the same two player optimization. Code (Text): loc_C322: ;Initcam_OOZ tst.w (Two_player_mode).w bne.w loc_C2F4 ;Initcam_HTZ lsr.w #3,d0 addi.w #$50,d0 move.w d0,($FFFFEE0C).w move.w d0,($FFFFEE2C).w clr.l ($FFFFEE08).w clr.l ($FFFFEE28).w rts Next we're gonna edit scroll event routines to skip over sequences like the boss load. Just add this branch and mini routine within LevEvents_OOZ2_Routine1: Code (Text): ; loc_F07C: LevEvents_OOZ2_Routine1: tst.w (Two_player_mode).w bne.s LevEvents_OOZ2_2P cmpi.w #$2668,(Camera_X_pos).w bcs.s return_F0A6 move.w (Camera_X_pos).w,(Camera_Min_X_pos).w move.w (Camera_X_pos).w,(Tails_Min_X_pos).w move.w #$2D8,(Object_RAM+$380+y_pos).w move.w #$1E0,(Camera_Max_Y_pos).w move.w #$1E0,(Tails_Max_Y_pos).w addq.b #2,(Dynamic_Resize_Routine).w return_F0A6: rts ; =========================================================================== LevEvents_OOZ2_2P: move.w #$2920,(Camera_Max_X_pos).w move.w #$2920,(Tails_Max_X_pos).w rts ; =========================================================================== ; loc_F0A8: LevEvents_OOZ2_Routine2: cmpi.w #$2880,(Camera_X_pos).w bcs.s return_F0EA Step Six coming soon.
Step 6: Fixing the layout Now we're gonna have to refine the object layout for OOZ as certain things don't load right in split screen. First of all make backup copies of your OOZ object files for 1P mode. The ball launchers: These freaking checkered balls are EVERYWHERE in the level and they sap up a ton of object space. They also have an issue with loading in split screen, which can lead to them breaking the sequence and leaving the player stuck rolling in the air. To fix this, go to every ball launcher and activate Long Distance and Remember State as True. Refining Long Distance loading: Oil Ocean is quite a bitch to get running properly in split screen due to its limitations loading objects in masses, which can overload it and cause it to stop loading them at all, softlocking the game. Once again our friends at Sock Team have made some coding to alleviate this and allow Long Distance to take a bigger load. Add this just before the DeleteObject routine: Code (Text): ; =========================================================================== ; input: a0 = the object ; loc_16472: MarkObjGone_LongDistance: tst.w (Two_player_mode).w bne.s .twoPlayers move.w x_pos(a0),d0 andi.w #$FF80,d0 sub.w (Camera_X_pos_coarse).w,d0 cmpi.w #$80+320+$40+$80,d0 ; This gives an object $80 pixels of room offscreen before being unloaded (the $40 is there to round up 320 to a multiple of $80) bhi.w .clrDespawn bra.w DisplaySprite .clrDespawn: lea (Object_Respawn_Table).w,a2 moveq #0,d0 move.b respawn_index(a0),d0 beq.s .delete bclr #7,2(a2,d0.w) .delete: bra.w DeleteObject ; --------------------------------------------------------------------------- ; input: a0 = the object ; loc_164A6: .twoPlayers: move.w x_pos(a0),d0 andi.w #$FF00,d0 addi.w #$100,d0 move.w d0,d1 sub.w (Camera_X_pos_coarse).w,d0 cmpi.w #$300,d0 bhi.w .tstPlayer2 bra.w DisplaySprite .tstPlayer2: sub.w ($FFFFF7DC).w,d1 cmpi.w #$300,d1 bhi.w .clrDespawn2P bra.w DisplaySprite .clrDespawn2P: lea (Object_Respawn_Table).w,a2 moveq #0,d0 move.b respawn_index(a0),d0 beq.s .delete2P bclr #7,2(a2,d0.w) .delete2P: bra.w DeleteObject ; =========================================================================== ; input: d0 = the object's x position ; loc_1640A: MarkObjGone2_2P: tst.w (Two_player_mode).w beq.s + bra.w DisplaySprite + andi.w #$FF80,d0 sub.w (Camera_X_pos_coarse).w,d0 cmpi.w #$80+320+$40+$80,d0 bhi.w + bra.w DisplaySprite + lea (Object_Respawn_Table).w,a2 moveq #0,d0 move.b respawn_index(a0),d0 beq.s + bclr #7,2(a2,d0.w) + bra.w DeleteObject Now we'll apply branches to this code for some of the objects that can work off it, namely the ball launchers as well as Octus and Aquis due to their missile spamming: Ball Launcher (Obj48): Code (Text): Obj48: moveq #0,d0 move.b routine(a0),d0 move.w off_25262(pc,d0.w),d1 jsr off_25262(pc,d1.w) move.b objoff_2C(a0),d0 add.b objoff_36(a0),d0 beq.w obj48markObjGone jmp JmpTo15_DisplaySprite obj48markObjGone: jmp (MarkObjGone_LongDistance).l Octus (Obj4A): Code (Text): loc_2CA34: bsr.w JmpTo19_ObjectMove lea (off_2CBDC).l,a1 bsr.w JmpTo13_AnimateSprite jmp MarkObjGone_LongDistance ;_2P Aquis (Obj50): Code (Text): loc_2CDF4: bsr.w JmpTo20_ObjectMove lea (off_2CF6C).l,a1 bsr.w JmpTo14_AnimateSprite jmp MarkObjGone_LongDistance ;p2 This should lessen the object load a fair bit, though you'll still likely need to ration things in areas. Keep toying with this until you have something that runs smoothly enough. Also remember to add a signpost at the end of Act 2, don't worry it won't load in one player mode. Once again I have left sample object files if you wanna quick setup. These also demonstrate some of the patch work objects I will leave the files for later in the tutorial, but I'm gonna be honest, I couldn't really fit them in there most of the time, so I'll leave it to your own coding prowess and sense of pragmatism what goes where. Step Seven coming soon.
Step Seven: Making separate object files for 1p and 2p mode: Now we have edits for 2p mode but the problem is these edits are now consistent with 1p mode. First rename your object files as "OOZ_1_2P.bin" and "OOZ_2_2P.bin" respectively. Also add back in your original backup object files unchanged. Now we're gonna code the game to switch between the two sets of object files depending on mode. Luckily the game already does this for Casino Night so we can just follow suit. Go to loc_17AB8 and add this branch: Code (Text): loc_17AB8: addq.b #2,(Obj_placement_routine).w move.w (Current_ZoneAndAct).w,d0 ; If level == $0F (ARZ)... ror.b #1,d0 ; then this yields $87... lsr.w #6,d0 ; and this yields $0002. lea (Off_Objects).l,a0 ; Next, we load the first pointer in the object layout list pointer index, movea.l a0,a1 ; then copy it for quicker use later. adda.w (a0,d0.w),a0 ; (Point1 * 2) + $0002 tst.w (Two_player_mode).w ; skip if not in 2-player vs mode beq.s loc_17AF0 ;beq.s cmpi.b #$C,(Current_Zone).w ; skip if not Casino Night Zone bne.s loc_17AB8_OOZ lea (Objects_CNZ1_2P).l,a0 ; CNZ 1 2-player object layout tst.b (Current_Act).w ; skip if not past act 1 beq.s loc_17AF0 lea (Objects_CNZ2_2P).l,a0 ; CNZ 2 2-player object layout loc_17AB8_OOZ: cmpi.b #$A,(Current_Zone).w ; skip if not Oil Ocean Zone bne.s loc_17AF0 lea (Objects_OOZ1_2P).l,a0 ; OOZ 1 2-player object layout tst.b (Current_Act).w ; skip if not past act 1 beq.s loc_17AF0 lea (Objects_OOZ2_2P).l,a0 ; OOZ 2 2-player object layout Now add your 2P object files into the code: Code (Text): ;--------------------------------------------------------------------------------------- ; CNZ act 1 object layout for 2-player mode (various objects were deleted) ;--------------------------------------------------------------------------------------- ; byte_1802A; Objects_CNZ1_2P: BINCLUDE "level/objects/CNZ_1_2P.bin" ;--------------------------------------------------------------------------------------- ; CNZ act 2 object layout for 2-player mode (various objects were deleted) ;--------------------------------------------------------------------------------------- ; byte_18492: Objects_CNZ2_2P: BINCLUDE "level/objects/CNZ_2_2P.bin" ;--------------------------------------------------------------------------------------- ; OOZ act 1 object layout for 2-player mode (various objects were deleted) ;--------------------------------------------------------------------------------------- ; byte_1802A; Objects_OOZ1_2P: BINCLUDE "level/objects/OOZ_1_2P.bin" ;--------------------------------------------------------------------------------------- ; OOZ act 2 object layout for 2-player mode (various objects were deleted) ;--------------------------------------------------------------------------------------- ; byte_18492: Objects_OOZ2_2P: BINCLUDE "level/objects/OOZ_2_2P.bin" With this done, both 1p and 2p should load separate object layouts. Continue editing your 2-player layout: If you still wanna keep refining your Oil Ocean layout separately for one player's, then add routines for them at the end of your SonLVL.ini file in your disassembly's SonLVL INIs folder. They should now appear as a separate option in the disassembly's level list in the program: Code (Text): [Oil Ocean Zone Act 1 - 2 Player] tiles=../art/kosinski/OOZ_2P.bin blocks=../mappings/16x16/OOZ_2P.bin chunks=../mappings/128x128/OOZ_2P.bin layout=../level/layout/OOZ_1_VS.bin objects=../level/objects/OOZ_1_2P.bin rings=../level/rings/OOZ_1.bin palette=../art/palettes/SonicAndTails.bin:0:0:16|../art/palettes/OOZ_2P.bin:0:16:48 colind1=../collision/OOZ primary 16x16 collision index.bin objlst=objOOZ.ini [Oil Ocean Zone Act 2 - 2 Player] tiles=../art/kosinski/OOZ_2P.bin blocks=../mappings/16x16/OOZ_2P.bin chunks=../mappings/128x128/OOZ_2P.bin layout=../level/layout/OOZ_2_VS.bin objects=../level/objects/OOZ_2_2P.bin rings=../level/rings/OOZ_2.bin palette=../art/palettes/SonicAndTails.bin:0:0:16|../art/palettes/OOZ_2P.bin:0:16:48 colind1=../collision/OOZ primary 16x16 collision index.bin objlst=objOOZ.ini Step Eight coming soon.
Step Eight: Making MORE room Even with all this rigamarole, we STILL need to jump a few extra hoops to get EVERYTHING to fit into Oil Ocean in split screen. We're gonna have to do something a bit more drastic in this case, and cut down some core objects. First download the objects files below, and allocate them to their respective folders in your disassembly. Thankfully these don't contradict nearly as bad with the one player version as in Chemical Plant, nor do they really have enough quality loss to be noticable in that mode, so the duplicate files can just replace your existing ones. Shield (Obj38): So the shield takes up less room, we're going to refine it to use dynamic pattern load cues. Go to Obj38 onwards and replace it all with this: Code (Text): Obj38_Main: addq.b #2,routine(a0) move.l #Obj38_MapUnc_1DBE4,mappings(a0) move.b #4,render_flags(a0) move.b #1,priority(a0) move.b #$18,width_pixels(a0) move.w #$4C2,art_tile(a0) bsr.w Adjust2PArtPointer ; loc_1D92C: Obj38_Shield: movea.w parent(a0),a2 ; a2=character btst #1,status_secondary(a2) bne.s return_1D976 btst #0,status_secondary(a2) beq.s JmpTo7_DeleteObject move.w x_pos(a2),x_pos(a0) move.w y_pos(a2),y_pos(a0) move.b status(a2),status(a0) andi.w #$7FFF,art_tile(a0) tst.w art_tile(a2) bpl.s Obj38_Display ori.w #$8000,art_tile(a0) ; loc_1D964: Obj38_Display: lea (byte_1DBD6).l,a1 jsr AnimateSprite bsr.w LoadShieldDynPLC jmp DisplaySprite ; =========================================================================== return_1D976: rts ; =========================================================================== JmpTo7_DeleteObject jmp DeleteObject ; --------------------------------------------------------------------------- ; Shield pattern loading subroutine ; --------------------------------------------------------------------------- ; ||||||||||||||| S U B R O U T I N E ||||||||||||||||||||||||||||||||||||||| ; loc_1D1AC: LoadShieldDynPLC: moveq #0,d0 moveq #0,d1 moveq #0,d2 moveq #0,d6 ; clear any cache data from data registers lea (ArtUnc_Shield_Unc).l,a1 ; load shield art into address register 1 lea (Shield_Tile_Arr).l,a2 ; load shield starting tile array into address register 2 move.w #$9840,d0 ; load VRAM number moveq #$B,d6 ; load number of total number of tiles to replace move.b mapping_frame(a0),d1 ; load frame number LoadShieldDynPLC_Part2: lea ($C00000).l,a6 ; load VDP control address into address register 6 lsl.l #2,d0 lsr.w #2,d0 ori.w #$4000,d0 swap d0 ; convert VRAM number to DMA VRAM address cmp.b #6,d1 ; is the mapping frame at 6? beq.w return_ShieldDPLC ; if yes, then end DPLC add.w d1,d1 ; convert size of fetch number (frame number) to array's index's number(starting tile) move.w (a2,d1.w),d2 ; transfer word size number data (starting tile) into data register 2 mulu.w #$20,d2 ; convert starting tile's number size into a size for the length of each tile in the art file ; ------------------------------------------------------------------------------ ; Loads any Uncompressed Art directly to VDP without the need of a DPLC ; Loads into 1 tile of space per loop (rept 8) ; ------------------------------------------------------------------------------ ShPLC_ReadEntry: move.l d0,4(a6) ; load VRAM DMA into VDP Control lea (a1,d2.w),a3 ; transfer the location of the starting pixels of the current tile into address register 3 move.l (a3)+,(a6) move.l (a3)+,(a6) move.l (a3)+,(a6) move.l (a3)+,(a6) move.l (a3)+,(a6) move.l (a3)+,(a6) move.l (a3)+,(a6) move.l (a3)+,(a6) ; load pixels into VRAM DMA by each pixel addi.l #$200000,d0 ; add on for the very next 32 pixels from VRAM DMA location addi.w #$20,d2 ; add on for the very next 32 pixels from the art file dbf d6,ShPLC_ReadEntry ; repeat depending on the number of tiles there are to change return_ShieldDPLC: rts ; -------------------------------------------------------------------------------------- Shield_Tile_Arr: dc.w 0, 4, 8, $C, $10, $14, $0 even Now edit the shield's animation entry as such: Code (Text): byte_1DBD6: dc.b 0, 2, 0, 6, 5, 6, 0, 6, 5, 6, 1, 6, 5 dc.b 6, 2, 6, 5, 6, 3, 6, 5, 6, 4, $FF even Then add in an entry for the new uncompressed art file: Code (Text): ;-------------------------------------------------------------------------------------- ; Nemesis compressed art (32 blocks) ; Shield ; ArtNem_71D8E: ArtNem_Shield: BINCLUDE "art/nemesis/Shield.bin" ;-------------------------------------------------------------------------------------- ; Nemesis compressed art (32 blocks) ; Shield ; ArtNem_71D8E: ArtUnc_Shield_Unc: BINCLUDE "art/uncompressed/Shield_Unc1.bin" Special thanks to Jdpense for the DPLC conversion and arranged mapping file. Invincibility stars (Obj35): For the stars, we're simply going to move them further up now we have more room. Go to it's vram entry like so: Code (Text): loc_1D9AE: _move.b 0(a0),0(a1) ; load obj35 move.b #4,objoff_A(a1) move.l #Obj35_MapUnc_1DCBC,mappings(a1) move.w #$4CE,art_tile(a1) bsr.w Adjust2PArtPointer2 move.b #4,render_flags(a1) bset #6,render_flags(a1) move.b #$10,objoff_E(a1) move.b #2,objoff_F(a1) move.w parent(a0),parent(a1) move.b d2,objoff_36(a1) addq.w #1,d2 move.l (a2)+,objoff_30(a1) move.w (a2)+,objoff_34(a1) lea next_object(a1),a1 ; a1=object dbf d1,loc_1D9AE move.b #2,objoff_A(a0) move.b #4,objoff_34(a0) Then edit the PLC entry to recognise the VRAM relocation, also edit out the original nemesis shield file since we are no longer using it. Code (Text): ;--------------------------------------------------------------------------------------- ; PATTERN LOAD REQUEST LIST ; Standard 2 - loaded for every level ;--------------------------------------------------------------------------------------- PlrList_Std2: plrlistheader plreq $8F80, ArtNem_Checkpoint plreq $D000, ArtNem_Powerups ;plreq $97C0, ArtNem_Shield plreq $99C0, ArtNem_Invincible_stars PlrList_Std2_End On the plus side, this also leaves some extra VRAM space throughout the entire game, meaning you also have an extra bit of room for Hill Top and Chemical Plant as well (might be handy if you wanna add back in the water surface art to the latter for example). Step Nine coming soon.
Step Nine: Fixing the art: Okay, now we have all the free space we need, we can start fixing up all the assets to load properly, and hooooo boy there's a lot to go through. 1. Animated tiles First we're going to get a more elaborate element out of the way. We've had to make several extra animated cues in the zone to make extra space. We also have to make a separate animation cue entry for Oil Ocean zone so it doesn't clash with the one player layout. Go to the routine that loads PLC_DYNANM and add a branch for a new VS routine. Code (Text): loc_3FCC4: tst.w (Two_player_mode).w bne.w loc_3FCC4_VS moveq #0,d0 move.b (Current_Zone).w,d0 add.w d0,d0 add.w d0,d0 move.w PLC_DYNANM+2(pc,d0.w),d1 lea PLC_DYNANM(pc,d1.w),a2 move.w PLC_DYNANM(pc,d0.w),d0 jmp PLC_DYNANM(pc,d0.w) ; =========================================================================== rts ; =========================================================================== loc_3FCC4_VS: moveq #0,d0 move.b (Current_Zone).w,d0 add.w d0,d0 add.w d0,d0 move.w PLC_DYNANM_VS+2(pc,d0.w),d1 lea PLC_DYNANM_VS(pc,d1.w),a2 move.w PLC_DYNANM_VS(pc,d0.w),d0 jmp PLC_DYNANM_VS(pc,d0.w) ; =========================================================================== rts ; =========================================================================== Now add the new branch routine underneath the PLC_DYNANM one: Code (Text): ; --------------------------------------------------------------------------- ; ZONE ANIMATION PROCEDURES AND SCRIPTS FOR VS MODE ; --------------------------------------------------------------------------- PLC_DYNANM_VS: ; Zone ID dc.w Dynamic_Normal-PLC_DYNANM_VS ; $00 dc.w Animated_EHZ-PLC_DYNANM_VS dc.w Dynamic_Null-PLC_DYNANM_VS ; $01 dc.w Animated_Null-PLC_DYNANM_VS dc.w Dynamic_Null-PLC_DYNANM_VS ; $02 dc.w Animated_Null-PLC_DYNANM_VS dc.w Dynamic_Null-PLC_DYNANM_VS ; $03 dc.w Animated_Null-PLC_DYNANM_VS dc.w Dynamic_Normal-PLC_DYNANM_VS ; $04 dc.w Animated_MTZ-PLC_DYNANM_VS dc.w Dynamic_Normal-PLC_DYNANM_VS ; $05 dc.w Animated_MTZ-PLC_DYNANM_VS dc.w Dynamic_Null-PLC_DYNANM_VS ; $06 dc.w Animated_Null-PLC_DYNANM_VS dc.w Dynamic_HTZ-PLC_DYNANM_VS ; $07 dc.w Animated_HTZ-PLC_DYNANM_VS dc.w Dynamic_Normal-PLC_DYNANM_VS ; $08 dc.w Animated_OOZ-PLC_DYNANM_VS dc.w Dynamic_Null-PLC_DYNANM_VS ; $09 dc.w Animated_Null-PLC_DYNANM_VS dc.w Dynamic_Normal-PLC_DYNANM_VS ; $0A dc.w Animated_OOZVS-PLC_DYNANM_VS dc.w Dynamic_Null-PLC_DYNANM_VS ; $0B dc.w Animated_Null-PLC_DYNANM_VS dc.w Dynamic_CNZ-PLC_DYNANM_VS ; $0C dc.w Animated_CNZ-PLC_DYNANM_VS dc.w Dynamic_Normal-PLC_DYNANM_VS ; $0D dc.w Animated_CPZ-PLC_DYNANM_VS dc.w Dynamic_Normal-PLC_DYNANM_VS ; $0F dc.w Animated_DEZ-PLC_DYNANM_VS dc.w Dynamic_ARZ-PLC_DYNANM_VS ; $10 dc.w Animated_ARZ-PLC_DYNANM_VS dc.w Dynamic_Null-PLC_DYNANM_VS ; $11 dc.w Animated_Null-PLC_DYNANM_VS ; yes, zone $11 This is a bit cleaner than the layout and object branches that allows you to make separate animation entries for ALL the levels in VS mode (you can likely make similar setups for said areas by following the same method). Now we need to both edit the 1 player OOZ animation entry (OOZ2 for some reason, OOZ is used for Hidden Palace) and add a new entry for VS mode. Find the OOZ2 routine and replace with this: Code (Text): ; word_400C8 ; Animated_OOZ: Animated_OOZ2: dc.w 5 ; Green flames from Obj33 zoneanimdecl 2, ArtUnc_OOZBurn, $5C40, 7, 8 dc.b $10 dc.b 0 dc.b $10 dc.b 0 dc.b $10 dc.b 0 dc.b 8 even ; Pulsing ball from OOZ zoneanimdecl -1, ArtUnc_OOZPulseBall, $56C0, 4, 4 dc.b 0 dc.b $B dc.b 4 dc.b 5 dc.b 8 dc.b 9 dc.b 4 dc.b 3 ; Square rotating around ball in OOZ zoneanimdecl 6, ArtUnc_OOZSquareBall1, $5740, 4, 4 dc.b 0 dc.b 4 dc.b 8 dc.b $C ; Square rotating around ball zoneanimdecl 6, ArtUnc_OOZSquareBall2, $57C0, 4, 4 dc.b 0 dc.b 4 dc.b 8 dc.b $C ; Oil zoneanimdecl $11, ArtUnc_Oil1, $5840, 6,$10 dc.b 0 dc.b $10 dc.b $20 dc.b $30 dc.b $20 dc.b $10 ; Oil zoneanimdecl $11, ArtUnc_Oil2, $5A40, 6,$10 dc.b 0 dc.b $10 dc.b $20 dc.b $30 dc.b $20 dc.b $10 Animated_OOZVS: dc.w 3 ; Green flames from Obj33 zoneanimdecl 2, ArtUnc_OOZBurn, $8A40, 7, 8 dc.b $10 dc.b 0 dc.b $10 dc.b 0 dc.b $10 dc.b 0 dc.b 8 even ; Pulsing ball from OOZ zoneanimdecl -1, ArtUnc_OOZPulseBall, $7200, 4, 4 dc.b 0 dc.b $B dc.b 4 dc.b 5 dc.b 8 dc.b 9 dc.b 4 dc.b 3 ; Square rotating around ball in OOZ zoneanimdecl 6, ArtUnc_OOZSquareBall1VS, $7280, 4, 4 ;$6C00 dc.b 0 dc.b 4 dc.b 8 dc.b $C ; Oil zoneanimdecl $11, ArtUnc_Oil1VS, $7000, 6,$10 ;$5840 dc.b 0 dc.b $10 dc.b $20 dc.b $30 dc.b $20 dc.b $10 Now add in the neccessary new uncompressed files for VS mode (the current layout uses less art files than one player though I added the others to the file just in case you manage to fit them in): Code (Text): ;--------------------------------------------------------------------------------------- ; Uncompressed art ; Square rotating around ball in OOZ ; ArtUnc_4C0FE: ArtUnc_4C2FE: ArtUnc_OOZSquareBall1: BINCLUDE "art/uncompressed/Square rotating around ball in OOZ - 1.bin" ArtUnc_OOZSquareBall2: BINCLUDE "art/uncompressed/Square rotating around ball in OOZ - 2.bin" ArtUnc_OOZSquareBall1VS: BINCLUDE "art/uncompressed/Square rotating around ball in OOZ - 1_2P.bin" ArtUnc_OOZSquareBall2VS: BINCLUDE "art/uncompressed/Square rotating around ball in OOZ - 2_2P.bin" ;--------------------------------------------------------------------------------------- ; Uncompressed art ; Oil in OOZ ; ArtUnc_4C4FE: ArtUnc_4CCFE: ArtUnc_Oil1: BINCLUDE "art/uncompressed/Oil - 1.bin" ArtUnc_Oil2: BINCLUDE "art/uncompressed/Oil - 2.bin" ArtUnc_Oil1VS: BINCLUDE "art/uncompressed/Oil - 1_2P.bin" ArtUnc_Oil2VS: BINCLUDE "art/uncompressed/Oil - 2_2P.bin" ;--------------------------------------------------------------------------------------- ; Uncompressed art ; Thin strip of falling oil in OOZ ; ArtUnc_OilFallVS_1: BINCLUDE "art/uncompressed/Cascading Oil VS_Small.bin" ;--------------------------------------------------------------------------------------- ; Uncompressed art ; Thick strip of falling oil in OOZ ; ArtUnc_OilFallVS_2: BINCLUDE "art/uncompressed/Cascading Oil VS_Large.bin" ;--------------------------------------------------------------------------------------- Also replace the entry for the nemesis burning flame art file with an uncompressed one: Code (Text): ;-------------------------------------------------------------------------------------- ; Nemesis compressed art (18 blocks) ; Green flame thing that shoots platform up in OOZ ; ArtNem_81514: even ArtUnc_OOZBurn: BINCLUDE "art/uncompressed/Green flame from OOZ burners.bin" ;-------------------------------------------------------------------------------------- 2. Object art For the objects, it's not as convoluted, just more tedious due to lots of individual branch work. First edit the PLC entries for OOZ in 1 player mode like such: Code (Text): ;--------------------------------------------------------------------------------------- ; Pattern load queue ; OOZ Primary ;--------------------------------------------------------------------------------------- PLC_10: plrlistheader plreq $5C40, ArtNem_OOZBurn plreq $7AC0, ArtNem_OOZElevator plreq $8780, ArtNem_SpikyThing plreq $6580, ArtNem_BurnerLid plreq $6640, ArtNem_StripedBlocksVert plreq $66C0, ArtNem_Oilfall plreq $68C0, ArtNem_Oilfall2 plreq $6A80, ArtNem_BallThing plreq $7300, ArtNem_LaunchBall PLC_10_End ;--------------------------------------------------------------------------------------- ; Pattern load queue ; OOZ Secondary ;--------------------------------------------------------------------------------------- PLC_11: plrlistheader plreq $6D00, ArtNem_OOZPlatform plreq $78A0, ArtNem_PushSpring plreq $7D00, ArtNem_OOZSwingPlat plreq $8080, ArtNem_StripedBlocksHoriz plreq $8180, ArtNem_OOZFanHoriz plreq $8680, ArtNem_Spikes plreq $8B80, ArtNem_VrtclSprng plreq $8E00, ArtNem_HrzntlSprng plreq $A000, ArtNem_Aquis plreq $A700, ArtNem_Octus PLC_11_End Now, go to the end of your PLC list and add these two routines for the split screen version. Code (Text): ;--------------------------------------------------------------------------------------- ; Pattern load queue ; OOZ Primary 2p ;--------------------------------------------------------------------------------------- PLC_10_VS: plrlistheader plreq $7AC0, ArtNem_OOZElevator plreq $8780, ArtNem_SpikyThing plreq $85C0, ArtNem_BurnerLid plreq $7300, ArtNem_LaunchBall PLC_10_VS_End ;--------------------------------------------------------------------------------------- ; Pattern load queue ; OOZ Secondary 2p ;--------------------------------------------------------------------------------------- PLC_11_VS: plrlistheader ;plreq $7380, ArtNem_OOZPlatform plreq $7880, ArtNem_PushSpring plreq $7D00, ArtNem_OOZSwingPlat plreq $8080, ArtNem_StripedBlocksHoriz plreq $8180, ArtNem_OOZFanHoriz plreq $8680, ArtNem_Spikes plreq $8B80, ArtNem_VrtclSprng plreq $8E00, ArtNem_HrzntlSprng plreq $9C00, ArtNem_Aquis plreq $E800, ArtNem_Octus PLC_11_VS_End Now go to your ArtLoadCues listing and add the new PLCs to the end of the list: Code (Text): ; word_42660 ; OffInd_PlrLists: ArtLoadCues: dc.w PlrList_Std1 - ArtLoadCues ; 0 dc.w PlrList_Std2 - ArtLoadCues ; 1 ...... dc.w PLC_38 - ArtLoadCues ; 64 dc.w PLC_39 - ArtLoadCues ; 65 dc.w PLC_3A - ArtLoadCues ; 66 dc.w PLC_10_VS - ArtLoadCues ; 67 ;$43 dc.w PLC_11_VS - ArtLoadCues ; 68 ;$44 And edit the 2 player entry for Oil Ocean Zone to recognise the new PLC cues: Code (Text): levartptrs $18,$19, $E, ArtKos_OOZ, BM16_OOZ, BM128_OOZ ; $A ; OOZ ; OIL OCEAN ZONE levartptrs $43,$44, $28, ArtKos_OOZ_2P, BM16_OOZ_2P, BM128_OOZ_2P ; $A ; OOZ ; OIL OCEAN ZONE (2 PLAYER) Now we get the joy of going through every single OOZ and editing the VRAM directs. Yay. (Thankfully no fiddly as hell subobjdata entries this time though). Elevator (Obj19): Code (Text): Obj19_Init: addq.b #2,routine(a0) ; => Obj19_Main move.l #Obj19_MapUnc_2222A,mappings(a0) move.w #$63A0,art_tile(a0) ; set default art cmpi.b #$A,(Current_Zone).w ; are we in OOZ? bne.s + ; if not, branch move.w #$63D6,art_tile(a0) ; set OOZ art + Falling oil and level patchwork (Obj1C): Probably the most complicated one due to how Obj1C handles it's files, thankfully it's just a case of adding new entries in (remember their sub names if you wanna place them in SonLVL though): Code (Text): dword_111E6: objsubdecl 0, Obj1C_MapUnc_11552, $43FD, 4, 6 ;0 objsubdecl 1, Obj1C_MapUnc_11552, $43FD, 4, 6 ;1 objsubdecl 1, Obj11_MapUnc_FC70, $43B6, 4, 1 ;2 objsubdecl 2, Obj1C_MapUnc_11552, $23FD, $10, 6 ;3 objsubdecl 3, Obj16_MapUnc_21F14, $43E6, 8, 4 ;4 objsubdecl 4, Obj16_MapUnc_21F14, $43E6, 8, 4 ;5 objsubdecl 1, Obj16_MapUnc_21F14, $43E6, $20, 1 ;6 objsubdecl 0, Obj1C_MapUnc_113D6, $4000, 8, 1 ;7 objsubdecl 1, Obj1C_MapUnc_113D6, $4000, 8, 1 ;8 objsubdecl 0, Obj1C_MapUnc_113EE, $4428, 4, 4 ;9 objsubdecl 0, Obj1C_MapUnc_11406, $4346, 4, 4 ;A objsubdecl 1, Obj1C_MapUnc_11406, $4346, 4, 4 ;B objsubdecl 2, Obj1C_MapUnc_11406, $4346, 4, 4 ;C objsubdecl 3, Obj1C_MapUnc_11406, $4346, 4, 4 ;D objsubdecl 4, Obj1C_MapUnc_11406, $4346, 4, 4 ;E objsubdecl 5, Obj1C_MapUnc_11406, $4346, 4, 4 ;F objsubdecl 0, Obj1C_MapUnc_114AE, $4346, $18, 4 ;10 objsubdecl 1, Obj1C_MapUnc_114AE, $4346, $18, 4 ;11 objsubdecl 2, Obj1C_MapUnc_114AE, $4346, 8, 4 ;12 objsubdecl 3, Obj1C_MapUnc_114AE, $4346, 8, 4 ;13 objsubdecl 4, Obj1C_MapUnc_114AE, $4346, 8, 4 ;14 ;New VS sprites objsubdecl 0, Obj1C_MapUnc_OOZVS, $44BA, 4, 4 ;15 objsubdecl 1, Obj1C_MapUnc_OOZVS, $44BA, 4, 4 ;16 objsubdecl 2, Obj1C_MapUnc_OOZVS, $44BA, 4, 4 ;17 objsubdecl 3, Obj1C_MapUnc_OOZVS, $44BA, 4, 4 ;18 objsubdecl 4, Obj1C_MapUnc_OOZVS, $44BA, 4, 4 ;19 objsubdecl 5, Obj1C_MapUnc_OOZVS, $44BA, 4, 4 ;1A objsubdecl 0, Obj1C_MapUnc_OOZVS2, $44BE, $18, 4;1B objsubdecl 1, Obj1C_MapUnc_OOZVS2, $44BE, $18, 4;1C objsubdecl 2, Obj1C_MapUnc_OOZVS2, $44BE, 8, 4;1D objsubdecl 3, Obj1C_MapUnc_OOZVS2, $44BE, 8, 4;1E objsubdecl 4, Obj1C_MapUnc_OOZVS2, $44BE, 8, 4;1F ;VS patchwork objsubdecl 0, Obj1C_MapUnc_OOZVS3, $6000, $40, 1;20 objsubdecl 1, Obj1C_MapUnc_OOZVS3, $6000, $40, 1;21 objsubdecl 2, Obj1C_MapUnc_OOZVS3, $6000, $40, 1;22 objsubdecl 3, Obj1C_MapUnc_OOZVS3, $6000, $10, 1;23 objsubdecl 4, Obj1C_MapUnc_OOZVS3, $6000, 8, 1;24 objsubdecl 5, Obj1C_MapUnc_OOZVS3, $4000, $10, 1;25 objsubdecl 6, Obj1C_MapUnc_OOZVS3, $4000, 8, 1;26 objsubdecl 7, Obj1C_MapUnc_OOZVS3, $6000, $80, 1;27 byte_1128E: dc.b 0 dc.b 0 ; 1 dc.b 0 ; 2 dc.b 0 ; 3 dc.b 0 ; 4 dc.b 0 ; 5 dc.b 0 ; 6 dc.b 0 ; 7 dc.b 0 ; 8 dc.b 0 ; 9 dc.b 0 ; 10/A dc.b 0 ; 11/B dc.b 0 ; 12/C dc.b $30 ; 13/D dc.b $40 ; 14/E dc.b $60 ; 15/F dc.b 0 ; 16/10 dc.b 0 ; 17/11 dc.b $30 ; 18/12 dc.b $40 ; 19/13 dc.b $50 ; 20/14 ;VS dc.b 0 ; 21/15 dc.b 0 ; 22/16 dc.b 0 ; 23/17 dc.b $30 ; 24/18 dc.b $40 ; 25/19 dc.b $60 ; 26/1A dc.b 0 ; 27/1B dc.b 0 ; 28/1C dc.b $30 ; 29/1D dc.b $40 ; 30/1E dc.b $50 ; 31/1F ;patchwork dc.b 8 ; 32/20 dc.b 8 ; 33/21 dc.b $10 ; 34/22 dc.b 8 ; 35/23 dc.b 8 ; 36/24 dc.b 8 ; 37/25 dc.b 8 ; 38/26 dc.b 8 ; 39/27 even Swinging platforms (Obj15): Code (Text): Obj15_Init: addq.b #2,routine(a0) move.l #Obj15_MapUnc_101E8,mappings(a0) move.w #$43E8,art_tile(a0) move.b #4,render_flags(a0) move.b #3,priority(a0) move.b #$20,width_pixels(a0) move.b #$10,y_radius(a0) move.w y_pos(a0),objoff_38(a0) move.w x_pos(a0),objoff_3A(a0) cmpi.b #$B,(Current_Zone).w bne.s loc_FD22 move.l #Obj15_Obj7A_MapUnc_10256,mappings(a0) move.w #0,art_tile(a0) move.b #$18,width_pixels(a0) move.b #8,y_radius(a0) Collapsing platforms (Obj1F): Code (Text): loc_10A5A: move.l a4,objoff_34(a0) cmpi.b #$A,(Current_Zone).w bne.s loc_10A86 move.l #Obj1F_MapUnc_110C6,mappings(a0) move.w #$6368,art_tile(a0) ;$639D tst.w (Two_player_mode).w beq.s Platform_1P move.w #$6000,art_tile(a0) ;$639D Platform_1P: bsr.w Adjust2PArtPointer move.b #$40,width_pixels(a0) move.l #byte_10C27,objoff_34(a0) Obj33 (Green hopping platform with flames): For this one we need to edit the entries for both objects: Code (Text): loc_23B08: addq.b #2,routine(a0) move.l #Obj33_MapUnc_23DDC,mappings(a0) move.w #$632C,art_tile(a0) tst.w (Two_player_mode).w beq.s GPlatform_1P move.w #$642E,art_tile(a0) GPlatform_1P: bsr.w JmpTo19_Adjust2PArtPointer move.b #4,render_flags(a0) move.b #3,priority(a0) move.b #$18,width_pixels(a0) move.w y_pos(a0),objoff_30(a0) addq.b #2,routine_secondary(a0) move.w #$78,objoff_36(a0) tst.b subtype(a0) beq.s loc_23B48 move.b #4,routine_secondary(a0) loc_23B48: bsr.w JmpTo7_SingleObjLoad2 bne.s loc_23B90 _move.b 0(a0),0(a1) ; load obj33 move.b #4,routine(a1) move.w x_pos(a0),x_pos(a1) move.w y_pos(a0),y_pos(a1) subi.w #$10,y_pos(a1) move.l #Obj33_MapUnc_23DF0,mappings(a1) move.w #$62E2,art_tile(a1) tst.w (Two_player_mode).w beq.s Flame_1P move.w #$6452,art_tile(a1) Flame_1P: bsr.w JmpTo4_Adjust2PArtPointer2 move.b #4,render_flags(a1) move.b #4,priority(a1) move.b #$10,width_pixels(a1) move.l a0,objoff_3C(a1) Obj3D (Breakable blocks for Ball launcher sequence): Code (Text): loc_24DE6: addq.b #2,routine(a0) move.l #Obj3D_MapUnc_250BA,mappings(a0) move.w #$6332,art_tile(a0) tst.b subtype(a0) beq.s loc_24E0A move.w #$6404,art_tile(a0) move.b #2,mapping_frame(a0) Obj3F (Fans): Code (Text): loc_2A7C4: addq.b #2,routine(a0) move.l #Obj3F_MapUnc_2AA12,mappings(a0) move.w #$640C,art_tile(a0) bsr.w JmpTo48_Adjust2PArtPointer ori.b #4,render_flags(a0) move.b #$10,width_pixels(a0) move.b #4,priority(a0) tst.b subtype(a0) bpl.s loc_2A802 addq.b #2,routine(a0) move.l #Obj3F_MapUnc_2AAC4,mappings(a0) bra.w loc_2A8FE Obj43 (Sliding spike obstacle): Code (Text): loc_23E66: addq.b #2,routine(a0) move.w #$C43C,art_tile(a0) bsr.w JmpTo19_Adjust2PArtPointer moveq #0,d1 move.b subtype(a0),d1 lea byte_23E54(pc,d1.w),a2 move.b (a2)+,d1 movea.l a0,a1 bra.s loc_23EA8 Obj48 (Ball launcher): Code (Text): loc_25276: addq.b #2,routine(a0) move.l #Obj48_MapUnc_254FE,mappings(a0) move.w #$6398,art_tile(a0) ;$6370 bsr.w JmpTo23_Adjust2PArtPointer move.b subtype(a0),d0 andi.w #$F,d0 btst #0,status(a0) beq.s loc_2529E addq.w #4,d0 Obj4A (Octus): For Octus we need to edit entries for both the badnik and his missile: Code (Text): loc_2CA52: move.l #Obj4A_MapUnc_2CBFE,mappings(a0) move.w #$2538,art_tile(a0) tst.w (Two_player_mode).w beq.w Octus_1P move.w #$2740,art_tile(a0) Octus_1P: ori.b #4,render_flags(a0) bsr.w JmpTo56_Adjust2PArtPointer move.b #$A,collision_flags(a0) Code (Text): loc_2CB70: jsr (SingleObjLoad).l bne.s return_2CBDA _move.b #$4A,0(a1) ; load obj4A move.b #6,routine(a1) move.l #Obj4A_MapUnc_2CBFE,mappings(a1) move.w #$2538,art_tile(a1) tst.w (Two_player_mode).w beq.w Octus_Missile_1P move.w #$2740,art_tile(a1) Octus_Missile_1P: bsr.w JmpTo4_Adjust2PArtPointer2 move.b #4,priority(a1) move.b #$10,width_pixels(a1) Aquis (Obj50): Same for Aquis and their missile: Code (Text): loc_2CCDE: addq.b #2,routine(a0) move.l #Obj50_MapUnc_2CF94,mappings(a0) move.w #$2500,art_tile(a0) tst.w (Two_player_mode).w beq.w Aquis_1P move.w #$24E0,art_tile(a0) Aquis_1P: ori.b #4,render_flags(a0) bsr.w JmpTo56_Adjust2PArtPointer move.b #$A,collision_flags(a0) move.b #4,priority(a0) move.b #$10,width_pixels(a0) move.w #-$100,x_vel(a0) move.b subtype(a0),d0 move.b d0,d1 andi.w #$F0,d1 lsl.w #4,d1 move.w d1,objoff_2E(a0) move.w d1,objoff_30(a0) andi.w #$F,d0 lsl.w #4,d0 subq.w #1,d0 move.w d0,objoff_32(a0) move.w d0,objoff_34(a0) move.w y_pos(a0),objoff_2A(a0) move.w (Water_Level_1).w,objoff_3A(a0) move.b #3,objoff_2E(a0) bsr.w JmpTo12_SingleObjLoad bne.s loc_2CDA2 _move.b #$50,0(a1) ; load obj50 move.b #4,routine(a1) move.w x_pos(a0),x_pos(a1) move.w y_pos(a0),y_pos(a1) addi.w #$A,x_pos(a1) addi.w #-6,y_pos(a1) move.l #Obj50_MapUnc_2CF94,mappings(a1) move.w #$2500,art_tile(a1) tst.w (Two_player_mode).w beq.w Aquis_Missile_1P move.w #$24E0,art_tile(a1) Aquis_Missile_1P: bsr.w JmpTo4_Adjust2PArtPointer2 ori.b #4,render_flags(a1) move.b #3,priority(a1) Code (Text): loc_2CE24: tst.b objoff_2D(a0) bne.w return_2CEAC st objoff_2D(a0) bsr.w JmpTo_loc_366D6 tst.w d1 beq.w return_2CEAC cmpi.w #-$10,d1 bcc.w return_2CEAC bsr.w JmpTo12_SingleObjLoad bne.s return_2CEAC _move.b #$50,0(a1) ; load obj50 move.b #6,routine(a1) move.w x_pos(a0),x_pos(a1) move.w y_pos(a0),y_pos(a1) move.l #Obj50_MapUnc_2CF94,mappings(a1) move.w #$2500,art_tile(a1) tst.w (Two_player_mode).w beq.w Aquis_Missile2_1P move.w #$24E0,art_tile(a1) Aquis_Missile2_1P: bsr.w JmpTo4_Adjust2PArtPointer2 ori.b #4,render_flags(a1) move.b #3,priority(a1) Note that this edit was made with the normal layout out of objects in mind. If you want to make use of the beta objects you'll have to do some switching around. Step Ten coming soon.
Step Ten: Fixing Bugs: Object display bugs: Certain objects have display branches that only work properly for single player mode, and won't spawn for Tails' screen. To fix this, we'll simply add branches in both their routines to skip around it: Floating Platform (obj19): Code (Text): ; loc_220B8: Obj19_Main: move.w x_pos(a0),-(sp) bsr.w Obj19_Move moveq #0,d1 move.b width_pixels(a0),d1 move.w #$11,d3 move.w (sp)+,d4 bsr.w JmpTo4_PlatformObject tst.w (Two_player_mode).w bne.s .displaySprite ; skip despawn check in 2-player mode move.w objoff_30(a0),d0 andi.w #$FF80,d0 sub.w (Camera_X_pos_coarse).w,d0 cmpi.w #$280,d0 bhi.w JmpTo20_DeleteObject .displaySprite: bra.w JmpTo11_DisplaySprite Sliding Spike Obstacle (Obj43): Code (Text): loc_23F0A: bsr.s loc_23F66 tst.w (two_player_mode).w bne.s JmpTo13_DisplaySprite ;skip despawn check in 2-player mode move.w objoff_32(a0),d0 andi.w #$FF80,d0 sub.w (Camera_X_pos_coarse).w,d0 cmpi.w #$280,d0 bls.s JmpTo13_DisplaySprite move.w objoff_34(a0),d0 andi.w #$FF80,d0 sub.w (Camera_X_pos_coarse).w,d0 cmpi.w #$280,d0 bhi.s loc_23F36 JmpTo13_DisplaySprite jmp DisplaySprite Make Octus recognise Tails: Octus by default only locks onto Sonic, and will just stupidly dance if only Tails is there. To fix this, add this entry for the sidekick player. Code (Text): loc_2CADE: move.w x_pos(a0),d0 sub.w (MainCharacter+x_pos).w,d0 cmpi.w #$80,d0 bgt.s loc_2CADE_Tails cmpi.w #-$80,d0 bge.s Octus_Shoot loc_2CADE_Tails: tst.w (Two_player_mode).w beq.s return_2CB02 ; will also react to Player 2 in two player mode move.w x_pos(a0),d0 sub.w (Sidekick+x_pos).w,d0 cmpi.w #$80,d0 bgt.s return_2CB02 cmpi.w #-$80,d0 blt.s return_2CB02 Octus_Shoot: addq.b #2,routine_secondary(a0) move.b #3,anim(a0) move.w #$20,objoff_2C(a0) return_2CB02: rts
Step 10: Just For Fun: If you want to add your own unique track for 2P OilOcean for that extra bit of authenticity, just go to 2P Music Playlist. This list solely edits what music tracks play for split screen conviniently enough so you can give Oil Ocean whatever theme you want without changing single player's. Code (Text): ;---------------------------------------------------------------------------- ; 2P Music Playlist ;---------------------------------------------------------------------------- ; byte_3EB2: MusicList2: dc.b $C+$80 ; 0 ; EHZ 2P dc.b 2+$80 ; 1 dc.b 5+$80 ; 2 dc.b 4+$80 ; 3 dc.b 5+$80 ; 4 dc.b 5+$80 ; 5 dc.b $F+$80 ; 6 dc.b 6+$80 ; 7 dc.b $10+$80 ; 8 dc.b $D+$80 ; 9 dc.b 4+$80 ; 10 ;OOZ 2P dc.b 3+$80 ; 11 ; MCZ 2P dc.b 8+$80 ; 12 ; CNZ 2P dc.b $E+$80 ; 13 dc.b $A+$80 ; 14 dc.b 7+$80 ; 15 dc.b $D+$80 ; 16 dc.b 0 ; 17 ; =========================================================================== And then, hopefully, we should FINALLY have Oil Ocean up and running. Now this was quite a convoluted one, there might be details I have missed out so be sure to tell me if something is amiss. Special thanx to Sock Team (especially Markeyjester and MoDule whose combined wizardry managed SOMEHOW managed to cram this damn level into split screen mode. Other VS mode tutorials: * Add Extra Item Options. * Add Hill Top Zone. * Make Custom Characters Work in Split Screen. * Add Chemical Plant Zone. * Make Water Work in Split Screen. * Toggle the Countdown.
This is neat, however I do have an actual suggestion this time: instead of using the new levels to replace existing ones in the level select screen, how about a tutorial on how to add more levels to it? There is a lot of empty space between the four quadrants, it can easily become a 3×2 or even a 4×2 grid with some rearranging. All the levels would be available at the same time in this way. That, or ditch the icons altogether and just add a new button combination to the regular level select screen, e.g. hold B or C while selecting a level to enable 2P mode if available, I seem to remember that one of the prototypes did something like this (though it's been years and I can't really check right now).
Expanding the VS menu is something I have considered looking into in the future. A big complication with that however is that you would also have to update other factors like the results screen according to how many new levels you have available. And yeah, the Simon Wai beta let you switch to split screen by holding down B and start when selecting I believe, though in that version, VS stipulations weren't added and it was more a co-operative affair through as much of the game as was compatible.
"Long Distance" OK, I think it is damn time to stop calling this flag "Long Distance". It is an awful name that doesn't describe what this flag is about, at all. As you may know, there are 2 distinct objects manager codes in Sonic 2. The 1P objects manager spawn all objects in range of the camera in the Dynamic_Object_RAM. The 1P objects manager doesn't destroy anything: all the objects are responsible for their own destruction (out of range, destroyed by the player...) The 2P objects manager works differently. Each player has 3 virtual columns of 2 chunks wide around them (6 chunks in total). Every object inside a column is spawned (all at once) by the 2P objects manager. Every object that leaves its column is destroyed (all at once) by the 2P objects manager. There is some code to manage overlapping of the 2 players columns to avoid spawning or destroying the objects when needed. But this is also an optimization to avoid lags. Objects in a column are all spawned, and are all destroyed at the same time, resulting in maybe 1 frame of lag the time it is done. The objects doesn't handle their own destruction, they doesn't need to check for bounds themselves, and this avoid wasting a lot of CPU resulting in more lags. About the storage in the Dynamic_Object_RAM, the 2P objects manager slices the Dynamic_Object_RAM into 6 parts of 12 objects each. Each part stores all the objects of a column. This means that in a column, if there is more than 12 objects, the 2P objects manager stops operating properly: the objects are no more spawned. That's why SEGA removed so many objects in the 2P CNZ, to avoid that from happening. And that's why levels like ARZ, or OOZ doesn't work out of the box in 2P mode. But that's not the end of the story. Objects sometimes spawn their own child objects, like a projectile (buzzer fireball for example). In this case, the child object is not spawned by the 2P objects manager and therefore is not part of any column. It fallbacks to the way it works in the 1P game: the child object is responsible for its own destruction. And this is fine in this case. About the storage, there is a reserved zone of the Dynamic_Object_RAM outside of the storage reserved for the columns. Every "dynamically" spawned objects like that fall in this zone (projectiles, scattered rings...). Let's say, every object that is not part of the objects list. Now, back on our "Long Distance" flag. This flag is only used in the 2P game. What this flag actually do is tell the 2P objects manager to NOT spawn the object "inside a column", but to spawn it as a "dynamic" object in the reserved zone of the Dynamic_Object_RAM. This means this object will not be destroyed by the 2P objects manager (it is not part of any column), but is responsible for its own destruction. It has to check itself for bounds. Only 2 objects use the "Long Distance" flag: buzzer of EHZ, and blowfly of MCZ. This is because these badniks can cross too many distance on screen and exit their assigned column by themselves. The result in this case is the badnik suddently disappearing off the screen... The "Long Distance" flag avoids that (and that's why it was named like that in the first place). But buzzer and blowfly, as "dynamic" objects, only checks themselves for their bounds to handle their own destruction. In the end, keep that in mind when using the "Long Distance" flag. "Long Distance" is misleading, it is good to know what this flag actually does. EDIT: On another subject, you can actually get rid of the Dynamic_Object_RAM slicing and the 12 objects limitation, by freeing one byte of the objects status table and assigning a "column ID" to manage the objects.
Fair point about 'long distance' though to be fair, I'm just keeping true to the labels to make it easier to follow. If it were labelled the 'Throat-warbler Mangrove' flag I would likely still refer to that label. :P The 'Column ID' approach sounds like it would be quite helpful as a VS modifying tool. How viable would that be for a tutorial?