Cool. If you guys get ARZ running properly, will you be willing to make it open source or do a tutorial like this one? I'd love to make as many levels as possible be available for other hackers to code into their projects. (I'll do a tutorial for CPZ too as soon as I figure out the damn thing).
Okay, I've figured out a tutorial friendly way to fix Spiker, and by that I mean it's doable but still pointlessly convoluted (Spiker's subobj data is ass to work with I can't redirect the VRAM location for the life of me). First take these mappings that relocate to a compatible part in the VRAM ($FC00) and put them in your mapping folder: https://cdn.discordapp.com/attachments/202861068224036864/735647209655107655/obj93_2P.bin Add a new subobjdata for your new mapping at the end of obj93's routine: Code (Text): ; off_3707C: Obj92_SubObjData: dc.l Obj92_Obj93_MapUnc_37092 dc.w 0 dc.w $404 dc.w $1012 Obj92_2P_SubObjData: dc.l Obj92_Obj93_MapUnc_2P dc.w 0 dc.w $404 dc.w $1012 ; animation script off_37086: dc.w byte_3708A-off_37086 dc.w byte_3708E-off_37086; 1 byte_3708A: dc.b 9, 0, 1,$FF byte_3708E: dc.b 9, 2, 3,$FF even ; --------------------------------------------------------------------------- ; sprite mappings ; --------------------------------------------------------------------------- Obj92_Obj93_MapUnc_37092: BINCLUDE "mappings/sprite/obj93.bin" ; --------------------------------------------------------------------------- ; sprite mappings ; --------------------------------------------------------------------------- Obj92_Obj93_MapUnc_2P: BINCLUDE "mappings/sprite/obj93_2P.bin" ; =========================================================================== Go to SubObjData_Index and add in your new subobj entry (if it's your first edit it will be entry $AE): Code (Text): SubObjData_Index: dc.w Obj8C_SubObjData - SubObjData_Index ; $0 dc.w Obj8D_SubObjData - SubObjData_Index ; $2 dc.w Obj90_SubObjData - SubObjData_Index ; $4 dc.w Obj90_SubObjData2 - SubObjData_Index ; $6 dc.w Obj91_SubObjData - SubObjData_Index ; $8 dc.w Obj92_SubObjData - SubObjData_Index ; $A dc.w Invalid_SubObjData - SubObjData_Index ; $C dc.w Obj94_SubObjData - SubObjData_Index ; $E dc.w Obj94_SubObjData2 - SubObjData_Index ; $10 dc.w Obj99_SubObjData2 - SubObjData_Index ; $12 dc.w Obj99_SubObjData - SubObjData_Index ; $14 dc.w Obj9A_SubObjData - SubObjData_Index ; $16 dc.w Obj9B_SubObjData - SubObjData_Index ; $18 dc.w Obj9C_SubObjData - SubObjData_Index ; $1A dc.w Obj9A_SubObjData2 - SubObjData_Index ; $1C dc.w Obj9D_SubObjData - SubObjData_Index ; $1E dc.w Obj9D_SubObjData2 - SubObjData_Index ; $20 dc.w Obj9E_SubObjData - SubObjData_Index ; $22 dc.w Obj9F_SubObjData - SubObjData_Index ; $24 dc.w ObjA0_SubObjData - SubObjData_Index ; $26 dc.w ObjA1_SubObjData - SubObjData_Index ; $28 dc.w ObjA2_SubObjData - SubObjData_Index ; $2A dc.w ObjA3_SubObjData - SubObjData_Index ; $2C dc.w ObjA4_SubObjData - SubObjData_Index ; $2E dc.w ObjA4_SubObjData2 - SubObjData_Index ; $30 dc.w ObjA5_SubObjData - SubObjData_Index ; $32 dc.w ObjA6_SubObjData - SubObjData_Index ; $34 dc.w ObjA7_SubObjData - SubObjData_Index ; $36 dc.w ObjA7_SubObjData2 - SubObjData_Index ; $38 dc.w ObjA8_SubObjData - SubObjData_Index ; $3A dc.w ObjA8_SubObjData2 - SubObjData_Index ; $3C dc.w ObjA7_SubObjData3 - SubObjData_Index ; $3E dc.w ObjAC_SubObjData - SubObjData_Index ; $40 dc.w ObjAD_SubObjData - SubObjData_Index ; $42 dc.w ObjAD_SubObjData2 - SubObjData_Index ; $44 dc.w ObjAD_SubObjData3 - SubObjData_Index ; $46 dc.w ObjAF_SubObjData2 - SubObjData_Index ; $48 dc.w ObjAF_SubObjData - SubObjData_Index ; $4A dc.w ObjB0_SubObjData - SubObjData_Index ; $4C dc.w ObjB1_SubObjData - SubObjData_Index ; $4E dc.w ObjB2_SubObjData - SubObjData_Index ; $50 dc.w ObjB2_SubObjData - SubObjData_Index ; $52 dc.w ObjB2_SubObjData - SubObjData_Index ; $54 dc.w ObjBC_SubObjData2 - SubObjData_Index ; $56 dc.w ObjBC_SubObjData2 - SubObjData_Index ; $58 dc.w ObjB3_SubObjData - SubObjData_Index ; $5A dc.w ObjB2_SubObjData2 - SubObjData_Index ; $5C dc.w ObjB3_SubObjData - SubObjData_Index ; $5E dc.w ObjB3_SubObjData - SubObjData_Index ; $60 dc.w ObjB3_SubObjData - SubObjData_Index ; $62 dc.w ObjB4_SubObjData - SubObjData_Index ; $64 dc.w ObjB5_SubObjData - SubObjData_Index ; $66 dc.w ObjB5_SubObjData - SubObjData_Index ; $68 dc.w ObjB6_SubObjData - SubObjData_Index ; $6A dc.w ObjB6_SubObjData - SubObjData_Index ; $6C dc.w ObjB6_SubObjData - SubObjData_Index ; $6E dc.w ObjB6_SubObjData - SubObjData_Index ; $70 dc.w ObjB7_SubObjData - SubObjData_Index ; $72 dc.w ObjB8_SubObjData - SubObjData_Index ; $74 dc.w ObjB9_SubObjData - SubObjData_Index ; $76 dc.w ObjBA_SubObjData - SubObjData_Index ; $78 dc.w ObjBB_SubObjData - SubObjData_Index ; $7A dc.w ObjBC_SubObjData2 - SubObjData_Index ; $7C dc.w ObjBD_SubObjData - SubObjData_Index ; $7E dc.w ObjBD_SubObjData - SubObjData_Index ; $80 dc.w ObjBE_SubObjData - SubObjData_Index ; $82 dc.w ObjBE_SubObjData2 - SubObjData_Index ; $84 dc.w ObjC0_SubObjData - SubObjData_Index ; $86 dc.w ObjC1_SubObjData - SubObjData_Index ; $88 dc.w ObjC2_SubObjData - SubObjData_Index ; $8A dc.w Invalid_SubObjData2 - SubObjData_Index ; $8C dc.w ObjB8_SubObjData2 - SubObjData_Index ; $8E dc.w ObjC3_SubObjData - SubObjData_Index ; $90 dc.w ObjC5_SubObjData - SubObjData_Index ; $92 dc.w ObjC5_SubObjData2 - SubObjData_Index ; $94 dc.w ObjC5_SubObjData3 - SubObjData_Index ; $96 dc.w ObjC5_SubObjData3 - SubObjData_Index ; $98 dc.w ObjC5_SubObjData3 - SubObjData_Index ; $9A dc.w ObjC5_SubObjData3 - SubObjData_Index ; $9C dc.w ObjC5_SubObjData3 - SubObjData_Index ; $9E dc.w ObjC6_SubObjData2 - SubObjData_Index ; $A0 dc.w ObjC5_SubObjData4 - SubObjData_Index ; $A2 dc.w ObjAF_SubObjData3 - SubObjData_Index ; $A4 dc.w ObjC6_SubObjData3 - SubObjData_Index ; $A6 dc.w ObjC6_SubObjData4 - SubObjData_Index ; $A8 dc.w ObjC6_SubObjData - SubObjData_Index ; $AA dc.w ObjC8_SubObjData - SubObjData_Index ; $AC dc.w Obj92_2P_SubObjData - SubObjData_Index ; $AE ; =========================================================================== Go to the start of Spiker (obj92)'s routine and edit it to recognise the new subtype: Code (Text): loc_36F24: tst.w (Two_player_mode).w beq.w loc_36F24_1P move.b #$AE,subtype(a0) ; <== Obj93_SubObjData2 loc_36F24_1P: bsr.w LoadSubObject bsr.w JmpTo64_Adjust2PArtPointer move.b #$40,objoff_2A(a0) move.w #$80,x_vel(a0) bchg #0,status(a0) rts Add an extra PLC cue at the end of the list (underneath PLC_3A) like so: Code (Text): ;--------------------------------------------------------------------------------------- ; Pattern load queue ; Tails end of level results screen ;--------------------------------------------------------------------------------------- PLC_3A: plrlistheader plreq $B000, ArtNem_TitleCard plreq $B600, ArtNem_ResultsText plreq $BE80, ArtNem_MiniTails plreq $A800, ArtNem_Perfect PLC_3A_End ;--------------------------------------------------------------------------------------- ; PATTERN LOAD REQUEST LIST ; Hill Top Zone 2P primary ;--------------------------------------------------------------------------------------- PlrList_Htz2P: plrlistheader plreq $73C0, ArtNem_Buzzer_Fireball plreq $7640, ArtNem_HtzRock plreq $78C0, ArtNem_HtzSeeSaw plreq $7BC0, ArtNem_Sol plreq $6FC0, ArtNem_Rexon plreq $EC00, ArtNem_Spiker plreq $8680, ArtNem_Spikes plreq $8780, ArtNem_DignlSprng plreq $8B80, ArtNem_VrtclSprng plreq $8E00, ArtNem_HrzntlSprng PlrList_Htz2P_End List it accordingly at the end of the ArtLoadCues (if it is the first new cue you have made it should be $43): Code (Text): ..... dc.w PLC_37 - ArtLoadCues ; 63 dc.w PLC_38 - ArtLoadCues ; 64 dc.w PLC_39 - ArtLoadCues ; 65 dc.w PLC_3A - ArtLoadCues ; 66 dc.w PlrList_Htz2P - ArtLoadCues ; 67 $43 Now edit your new 2 player LevelArtPointers to read for your new PLC list: Code (Text): ..... levartptrs $12,$13, $B, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ; 7 ; HTZ ; HILL TOP ZONE levartptrs $43,$13, $B, ArtKos_EHZ, BM16_EHZ, BM128_EHZ_2P ; 7 ; HTZ ; HILL TOP ZONE (2 PLAYER) I don't think this was worth the hassle at all just to fix one badnik (hackers that know good VRAM tricks can likely can find a much easier way) though I suppose this trick can also come in handy if you wanna fiddle around with more things between the 1 player and 2 player art files. Anyway that's hopefully the HTZ tutorial done with, and at least now I can make steps to converting other subobjs like those in CPZ.
As MoDule mentioned, I have fixed/optimised the H-blank routines (or rather rewrote them), while MoDule has been looking into fixing the background scrolling without drawing (as you'll be able to see with CPZ below). I took a look at those CPZ ROMs you shared, I was impressed with the idea of seeing CPZ in two-player mode, but I was a little disappointed with the curves, now, don't get me wrong, I understand why it was done but I really think we can do much better... Contrary to popular belief, I didn't do these by hand, these were created by a tool I wrote. It runs through the layout, checks through every chunk used and every block on that chunk used, and copies the two tiles from the top and bottom of every block into an 8x16 buffer, if a tile happens to be flipped/mirrored, it'll copy the flip/mirror version onto the 8x16 visually flipped but now both represented as "normal", if the two tiles happen to use different palette lines, it'll attempt to convert one of the tiles to use the other palette finding the closest coloured pixels possible (but will ignore cycle/backdrop colours), and it'll convert the tile which'll end up less damaged. While building up an array of 8x16 tiles, it'll omit duplicates (including different palette lined or flipped/mirrored). If the array is bigger than the original tile file size (which 90% of the time is the case) it'll go into a brute force mode and attempt to omit near matching 8x16 tiles (in a lossy form), done by comparing itself against the others and finding the one where the colours of each pixel are an almost match (you can see on some of the curves in CPZ and ARZ in the screenshot, it found the closest substitute 8x16 tiles which aren't too bad considering), but it'll usually omit the 8x16 tiles which are least displayed in the entire level so keep that in mind, some of the 8x16's are obvious, some are actually unnotice-able, so deciding which ones to omit is more complicated than just choosing the least displayed tile, might arrange it such that you can specify which tiles are susceptible for replacement. We can also do it on a "per act" basis too to cut down on tiles. While it's not perfect, it may provide an acceptable foundation for an artist to go and tweak it, though the artist might not have any tiles left, they could certainly do a better job and making the tile look correct in both circumstances. The best thing is, the blocks are altered tile-wise, but not shifted/changed, so the exact same collision, chunks and layout can be used, and don't need to be substituted for flat collision. Though, we do have yet to get around another limitation, the loss of 1000 bytes of VRAM caused by the second player's plane A, so the results might end up worse as time goes on, but we'll see, this is in testing situations for now. We are considering the idea of fusing both players on the same plane and using H-blank to wrap, but we're not sure on the CPU ramifications just yet, every interrupt costs a lot of time... We'll be looking into making the tool more GUI and user friendly with a few variable options, might also include the ability to create a fused 8x16 tile which is a lossy of both it's trying to fuse. But we'll also be looking into which method might be the most suitable for the 2-player mode, whether it be fusing the plane's or sacrificing and modifying the tiles more aggressively, time will tell.
That is...astonishing. I'm really glad you guys are on board with this conversion idea, since I really don't have much idea what to do in some areas. I can see some odd areas where it misdirects art but it is impressive so much detail can be preserved. I've started over with CPZ and have managed to figure out how to structure and fix most of CPZ's objects to work properly in both 1p and 2p mode now and streamlined the process a bit (but of course object numbers are still limited) so if we manage to get the level art loading and scrolling competently. I can make a tutorial for that level as well.
Ooh that's clever. But if it's a case of just not having enough tiles to work with, I bet there will be clever-er ways of gaining a few without anyone noticing. Like removing one of the background buildings in Chemical Plant, or some of the little bubbles in those blue tubes. It would be a fascinating exercise.
Seeing as this seems to be a community project now, and as some have already seen on Discord, I decided to focus on a different aspect of this, namely the zone select and player choice. The zone select is based on the one from Sonic 2 mobile, with some additions, namely the win counters and the item type choice from the options menu. Player character choice is done by pressing C on the appropriate controller. Player choice working in-game has been a bit of a pain, as a lot of things are hardcoded to expecting Sonic to be Player 1 and Tails to be Player 2, so it's been fun having to rearrange some things and add some branches to the others' code in others. It's still a work in progress, but it's getting there.
Might I suggest we setup a Discord server for those of us working on this and come to some sort of collaborative guide or pre-built disassembly for this?
Are you going to release that tool, because MAN, that would speed up the creation of 2P versions of levels to an insane degree!
I am impressed how fast this project got traction and how everyone here is on board with working on this to see how many zones can actually work in the 2P mode, its very exciting! I have always wondered how many stages in Sonic 2 could work in the 2P mode so this is pretty awesome stuff guys!!
We've got a team together working on things now. Things are looking hopeful (though I'm pretty much the slowest of these guys :P). I am rather delighted this snowballed into a proper project I must say. 2P has been neglected for so long.
Well shit I just found out I made an error with the tutorial this whole time (dunno how really, I didn't do this in my sample version). You have to change a flag check before skipping to the 2p routine in Dynamic_HTZ otherwise it can cause some display issues with 2 player's screen: Code (Text): Dynamic_HTZ: lea ($FFFFF7F0).w,a3 tst.w (Two_player_mode).w bne.w Dynamic_HTZ_2P moveq #0,d0 move.w (Camera_X_pos).w,d1 neg.w d1 asr.w #3,d1 move.w (Camera_X_pos).w,d0 move.w d0,d2 andi.w #$F,d2 seq.b d2 ext.w d2 lsr.w #4,d0 add.w d1,d0 add.w d2,d0 ....
Just another update. MoDule has managed to make a fix so the airlifts will still respawn set in long distance/remember state mode. First go to loc_21E10 and redirect the despawning branch to MarkObjGone_P1 instead of JmpTo5_MarkObjGone: Code (Text): loc_21E10: move.w x_pos(a0),-(sp) bsr.w loc_21E2C moveq #0,d1 move.b width_pixels(a0),d1 move.w #-$28,d3 move.w (sp)+,d4 bsr.w JmpTo3_PlatformObject jmp (MarkObjGone_P1).l Next we're gonna have this object's routine handle the spawning behaviour for the bridge poles as well, go to loc_21E7C and edit the loading branch for obj1C into this: Code (Text): loc_21E7C: bsr.w JmpTo4_ObjectMove subq.w #1,objoff_34(a0) bne.s return_21EC0 addq.b #2,routine_secondary(a0) move.b #2,mapping_frame(a0) move.w #0,x_vel(a0) move.w #0,y_vel(a0) bsr.w JmpTo4_SingleObjLoad2 bne.s return_21EC0 _move.b #$16,0(a1) ; load obj16 move.w x_pos(a0),x_pos(a1) move.w y_pos(a0),y_pos(a1) move.b render_flags(a0),render_flags(a1) move.b #4,routine(a1) ; => Obj16_Vines move.l #Obj16_MapUnc_21F14,mappings(a1) move.w #$43E6,art_tile(a1) jsr Adjust2PArtPointer2 ori.b #4,render_flags(a1) move.b #$20,width_pixels(a1) move.b #1,mapping_frame(a1) move.b #1,priority(a1) return_21EC0: rts Now just put a despawning direct for the replacement vine object under return_21ECO: Code (Text): ; =========================================================================== ; Replaces use of Obj1C Obj16_Vines: jmp MarkObjGone_P1 Don't forget to add this as a new routine too: Code (Text): ; =========================================================================== off_21DBA: dc.w loc_21DBE-off_21DBA dc.w loc_21E10-off_21DBA; 1 dc.w Obj16_Vines-off_21DBA; 2 ; =========================================================================== Again special thanx to MoDule for this fix.