From my understanding, dynamic pattern load cues load different art tiles depending on the frame of an object, replacing the tiles for the previous frame. This allows only cartridge space to limit the amount of art tiles an object uses (besides the number of art tiles in one frame) instead of VRAM but you can't have something else on screen that uses the same art but a different frame because the overwritten tiles will make it look messed up. This is used for Sonic in Sonic 1, Sonic and Tails in Sonic 2, and Sonic, Tails, Knuckles, Shields, and other stuff in Sonic 3. In my Sonic 1 hack (Hivebrain), I want to use these for other objects (mainly shields and the giant ring) but I can't figure out how I can use it. Do you know a way I can get it to work or at least give me something I can work off of? I created a topic instead of posting in the Basic Q&A thread because there may be several replies and, if answered, will be easier to find by someone searching in the future.
I'm not going to talk about how a Sonic engine would do it, or how you would do it using a subroutine or system in any Sonic game. Instead, I'm going to explain how it would be done through any type of game working on the Mega Drive. I believe, that by teaching you this way, would set you on the path to doing more extraordinary things, as opposed to just shield swapping or whatever... Though I will keep it rather basic. The VRAM is 10000 bytes in size (0000 - FFFF), each 20 byte is considered a potential tile. We'll write one tile of art data into VRAM from F300 - F31F (20 bytes). The first thing to do, is set the VDP mode/address. Two words are written to the VDP control port, here it is as a longword in binary: 10NM LKJI HGFE DCBA ---- ---- 5432 --POThe numbers are the mode, here is a list: 543210 VRAM Read: 000000 CRAM Read: 001000 VSRAM Read: 000100 VRAM Write: 000001 CRAM Write: 000011 VSRAM Write: 000101 We want to write to VRAM, so VRAM Write mode is used: 01NM LKJI HGFE DCBA ---- ---- 0000 --POThe starting address is F300, broken into binary that is 1111 0011 0000 0000. PONM LKJI HGFE DCBA F300: 1111 0011 0000 0000 This binary pattern in set in place of the letters: 0111 0011 0000 0000 ---- ---- 0000 --11The "-" symbols are invalid binary slots, effectively unused as far as we're aware, thus, they remain blank: 0111 0011 0000 0000 0000 0000 0000 0011Converted to hexadecimal, this is 73000003, and is what is needed to be saved to the VDP's control port (C00004/C00006): move.l #$73000003,($C00004).lThe VDP is now in VRAM write mode. Any data you write into the data port (C00000/C00002), will be copied into VRAM starting from F300 onwards: move.l #$11111111,($C00000).l Code: move.l #$11000011,($C00000).l Code: move.l #$10100101,($C00000).l Code: move.l #$10011001,($C00000).l Code: move.l #$10011001,($C00000).l Code: move.l #$10100101,($C00000).l Code: move.l #$11000011,($C00000).l Code: move.l #$11111111,($C00000).l That is one tile, a box with an X inside of it. This is known as a manual transfer, the data is being sent manually (controlled by the 68k). The second known method is trough a "DMA" transfer (Direct Memory Addressing). Where you supply the VDP the information about the data (where it is, how big it is, and where it's going to), and the VDP will stop the 68k, and transfer the data automatically and directly. As an example, we'll pretend that our tile is in 68k RAM at 8000 - 801F, and we're transfering it to VRAM at F300 - F31F. The VDP has a series of mode registers, which can be altered to set a desired display mode (be it resolution, plane size, name table addressing, etc), but the registers we'll be interested in, are the DMA registers. To write data to a VDP register, a word of data containing the register number and the byte to send to that register, are sent to the VDP control port (C00004/C00006): move.w #$8020,($C00004).lThe above example sends 20 to VDP register 80 (or register 00 if you prefer, the +80 is mearly a binary trigger). The registers are from 80 to 97 (00 - 17). Registers 93 and 94 are the DMA size: move.l #$94009310,($C00004).lIn the above example, we're sending data to two registers 10 to register 93 and 00 to register 94. The 00 10 together forms a word, this is the DMA size divided by 2 (0020 bytes / 2 = 0010), the division by two is due to an autoincrement register, but that's not important at this point in time. Registers 95, 96 and 97 are the DMA source: move.l #$96C09500,($C00004).l Code: move.w #$97[U]7F[/U],($C00004).l In the above example, we're sending data to three registers, 00 to register 95, C0 to register 96, and 7F to register 97. The 7F C0 00 together forms a 24-bit address, divided by 2 (FF8000 / 2 = 7FC000). 68k address FF8000 is 68k RAM 8000. Now that the registers are set, the destination and mode needs to be set, this is the same as setting VRAM Write mode for manual transfer, but there are two differences: move.w #$7300,($C00004).l Code: move.w #$0083,($C00004).l The long-word being sent is 73000083. This is similar to the 73000003 we sent for a manual transfer, except bit "5" of the binary mode is set to signify the address is for DMA transfer (this triggers DMA to start). The second difference is that it's been split up and sent off a word at a time. Where you write the first word doesn't matter, but the second word of the write address/mode MUST be written to C00004-C00005 instead of C00006-C00007. Otherwise there's a strong posibility that the first word shall not be transfered. As soon as that last word is written, the VDP will halt the 68k, and the 68k will not continue until the transfer is complete.
For starters read through this thread, it proved a useful read when I strived for the same goal. Then you want to read through the S3K shield routines and see how they use PLCLoad_Shields, most of the lines related to it are commented.
I found out that GreenSnake's multiple character guide replaced LoadSonicDynPLC with a system where there is a piece of code that loads the art, art location, and DPLC's and then branches to a piece of code that queues it and a piece of code that loads different art, mappings, DPLC's and animations depending on the character. I used this code to work with shields but it ended up messing up the game and the levels wouldn't even run unless I put the branch to the code that loads the DPLC's right after the beginning of the shield code. I also used the guide in the thread Pokepunch linked to. I've actually been unable to work with GreenSnake's guide when just using it for multiple characters no matter how many times I've tried to readd the code or what version of the guide I choose (I currently use the second one) because selecting a character other than Sonic yields me results similar to when I tried the shields. The problem may be because I use the 128 variation of the Hivebrain disassembly but I don't think so. Is there some way I can get it to work? Edit: The art isn't getting screwed up anymore because I removed an unnecessary branch to QueueDMATransfer. Unfortunately the shield art doesn't load and half the levels will infinitely reload when I enter them. Edit 2: I noticed that Sonic 3K splits each shield type into an object and saves the DPLC's in a similar way as mappings. I've decided to try that as apposed to using checks and tables and maybe use it for multiple characters too. I'll tell how it goes tomorrow (It's currently 11:18 at night and I have school tomorrow).
So I decided that before I make a bunch of objects, I'll try to get the DPLC's working with just one object and one set of art, mappings, and animations. The game seems to run fine but the shields don't display any art. Here is the code for Obj38: Spoiler ; =========================================================================== ; --------------------------------------------------------------------------- ; Object 38 - Shield ; --------------------------------------------------------------------------- Obj38: ; XREF: Obj_Index jmp LoadShieldDynPLC moveq #0,d0 move.b $24(a0),d0 move.w Shield_Index(pc,d0.w),d1 jmp Shield_Index(pc,d1.w) ; =========================================================================== Shield_Index: dc.w Shield_Init-Shield_Index dc.w ShieldChecks-Shield_Index ; =========================================================================== Shield_Init: addq.b #2,$24(a0) move.l #Map_lshield,4(a0) ; Load Shield Map into place move.b #4,1(a0) move.b #1,$18(a0) move.b #$18,$19(a0) move.w #$541,2(a0) ; Set VRAM location btst #7,($FFFFD002).w beq.s ShieldChecks bset #7,2(a0) ; --------------------------------------------------------------------------- ShieldChecks: tst.b ($FFFFFE2C).w ; Test if Sonic has a shield bne.s SonicHasShield ; If so, branch to do nothing tst.b ($FFFFFE2D).w ; Test if Sonic got invisibility beq.s jmp_Deleteobj38 ; If so, delete object temporarily ShieldProperties: move.w ($FFFFD008).w,8(a0) ; Load Main Character X-position move.w ($FFFFD00C).w,$C(a0) ; Load Main Character Y-position ; move.b ($FFFFD022).w,$22(a0) ; Something about Character status lea (Ani_obj38).l,a1 jsr AnimateSprite jmp DisplaySprite SonicHasShield: rts jmp_DeleteObj38: ; loc_12648: jmp DeleteObject Here's the code for loading DPLC's Spoiler Code: LoadShieldDynPLC: movea.l (LShieldDynPLC).l,a2 ; get DPLC location move.w #$A820,d4 ; offset in VRAM to store art move.l (UnC_LightningShield).l,d6 ; get art location moveq #0,d0 move.b $1A(a0),d0 ; load frame number cmp.b $FFFFF601.w,d0 ; check if equal with last queued frame beq.s DPLC_End ; if is, don't load new DPLC move.b d0,$FFFFF601.w ; remember queued frame ; End of function LoadShieldDynPLC ; --------------------------------------------------------------------------- ; Subroutine to queue any pattern load cue ; Input: a2 - DPLC file, d4 - VRAM address, d6 - Art file, d0 - frame number ; --------------------------------------------------------------------------- Load_DPLC: add.w d0,d0 ; multiply by 2 adda.w (a2,d0.w),a2 ; get the right DPLC location moveq #0,d5 ; quckly clear d5 move.b (a2)+,d5 ; then move the amount of requests to d5 subq.w #1,d5 ; subtract 1 bmi.s DPLC_End ; if negative, branch away DPLC_ReadEntry: moveq #0,d1 move.b (a2)+,d1 ; get first byte to d1, and increment pointer lsl.w #8,d1 ; shift 8 bits left move.b (a2)+,d1 ; move second byte to d1 move.w d1,d3 ; move d1 to d3 lsr.w #8,d3 ; shift 8 bits right andi.w #$F0,d3 ; leave only bits 7, 6, 5, and 4 addi.w #$10,d3 ; add $10 to d3 andi.w #$FFF,d1 ; filter out bits 15, 14, 13 and 12 lsl.l #5,d1 ; shift 5 bits left add.l d6,d1 ; add the art address to d1 move.w d4,d2 ; move VRAM location to d2 add.w d3,d4 ; add d3 to VRAM address add.w d3,d4 ; add d3 to VRAM address jsr QueueDMATransfer; Save it to the DMA queue dbf d5,DPLC_ReadEntry; repeat for number of requests DPLC_End: rts ; return
I'm not sure if it makes any difference, but I'd personally place the branch to the DPLC code just before display sprite. Having it before everything means it will run without the necessary information for the first frame, seems kinda pointless. Also all of my shields are one object, so it's possible albeit a tad messy.
Doing that will make the game crash when the level starts. It has to be at the beginning unless someone can figure out how to keep it from crashing.
I am not sure if this is the actual problem, but this is certainly wrong: movea.l (LShieldDynPLC).l,a2 ; get DPLC location move.w #$A820,d4 ; offset in VRAM to store art move.l (UnC_LightningShield).l,d6 ; get art location it should be as follows: lea (LShieldDynPLC).l,a2 ; get DPLC location move.w #$A820,d4 ; offset in VRAM to store art move.l #UnC_LightningShield,d6 ; get art location The thing is, unlike on Sonic object, we don't get the address from RAM, but instead constants, and we need to slightly alter how its loaded (and this slight alteration is vital). Another thing I suggest is using object SST for checking if the frame is loaded (instead of static RAM address). I personally use $3A but its up to you. And in fact the branch to DPLC loading is often most useful after AnimateSprite and before DisplaySprite. If you still have problems, let me know by messaging me and I see what I can do
Attached is a clean disassembly with the code and files I used. The shield still doesn't show up but it allows you to play levels fine without the jump to LoadShieldDynPLC at the beginning. The invincibility stars are messed up but thats because added art from my own hack that doesn't work with the mappings and just added it for the sake of having uncompressed invincibility stars art. There is also art and mappings and stuff for the bubble and flame shield but they aren't used and should not be bothered with (the DPLC's are also blank). I'd appreciate it if you could look over it and find an flaws that I may have missed. View attachment Sonic128test.zip
You know, if I remember correctly, the spindash dush uses DPLCs, and the spindash dust is an object, like I said, iirc. So you could look at that. But it's Sonic 2's code, and that's...yeah that's just blah.
I think I found out the issue, and there is 2 of them: lea (Ani_obj38).l,a1 jsr AnimateSprite jmp LoadShieldDynPLC ; <- ??? jmp DisplaySprite ; this is never reached Another was your DPLC is still incorrect, just switching it with one from my disassembly fixed the graphical problems of the shield and in fact it seems to work just about fine!
I fixed the DPLC file (it turns out that it uses player DPLC's instead of object DPLC's) but I still don't understand why display sprite is never gotten to and what I can do to fix it.
its simply because you just the "jmp" instruction instead of "jsr" on "LoadShieldDynPLC". JMP simply JuMP's to address, meaning it will never return, whereas "JSR" goes to a subroutine, and starts execution on the very next instruction. I recommend reading MarkeyJester's 68k tutorial, specifically about JMP and JSR.
Awesome! Thanks GreenSnake. Edit: The problem where the game crashed unless the code for the shield is blank (or if it branched to somewhere different where it wouldn't return was caused by a very stupid branch to the code that I added in the level loading routine.