i wish i knew about this when i still made rom hacks....... it was so so annoying how much ihad to worry about getting suddenly stopped on slopes, i never knew why it happened and it made designing levels so so annoying... anyways this fix is cool as hell LOL !
There is an oversight with invisible solid block collision. At the top of its main routine, it checks if it's offscreen: Code (Text): Invis_Solid: ; Routine 2 bsr.w ChkObjectVisible bne.s .chkdel There is a problem though. If we go to the routine: Code (Text): ; --------------------------------------------------------------------------- ; Subroutine to check if an object is off screen ; output: ; d0 = flag set if object is off screen ; --------------------------------------------------------------------------- ; ||||||||||||||| S U B R O U T I N E ||||||||||||||||||||||||||||||||||||||| ChkObjectVisible: move.w obX(a0),d0 ; get object x-position sub.w (v_screenposx).w,d0 ; subtract screen x-position bmi.s .offscreen cmpi.w #320,d0 ; is object on the screen? bge.s .offscreen ; if not, branch move.w obY(a0),d1 ; get object y-position sub.w (v_screenposy).w,d1 ; subtract screen y-position bmi.s .offscreen cmpi.w #224,d1 ; is object on the screen? bge.s .offscreen ; if not, branch moveq #0,d0 ; set flag to 0 rts .offscreen: moveq #1,d0 ; set flag to 1 rts ; End of function ChkObjectVisible It doesn't take into account the size of the hitbox, just the position. Now, it's not very common at all to run into this issue, but you can easily see the effect if you duck the camera down in this area: To fix this, add this routine that accounts for the hitbox: Code (Text): ; --------------------------------------------------------------------------- ; Subroutine to check if an object is off screen ; Takes both width and height into account ; output: ; d0 = flag set if object is off screen ; --------------------------------------------------------------------------- ; ||||||||||||||| S U B R O U T I N E ||||||||||||||||||||||||||||||||||||||| ChkSizedObjVisible: moveq #0,d1 ; Get object's width move.b obActWid(a0),d1 move.w obX(a0),d0 ; Get object's X position sub.w (v_screenposx).w,d0 ; Get object's X position on screen add.w d1,d0 ; Is the right side of the object on screen? bmi.s .offscreen2 ; If not, branch add.w d1,d1 ; Is the left side of the object on screen? sub.w d1,d0 cmpi.w #320,d0 bge.s .offscreen2 ; If not, branch moveq #0,d1 ; Get object's height move.b obHeight(a0),d1 move.w obY(a0),d0 ; Get object's Y position sub.w (v_screenposy).w,d0 ; Get object's Y position on screen add.w d1,d0 ; Is the bottom side of the object on screen? bmi.s .offscreen2 ; If not, branch add.w d1,d1 ; Is the top side of the object on screen? sub.w d1,d0 cmpi.w #224,d1 bge.s .offscreen2 ; If not, branch moveq #0,d0 ; Visible rts .offscreen2: moveq #1,d0 ; Not visible rts And change the call to ChkObjectVisible to ChkSizedObjVisible instead, and tada!
Small bug that's probably really insignificant in the grand scheme of things, but I thought it would be worth documenting. The plasma balls in Final Zone's movement code when it first gets spawned has a small bug in it. If we go to loc_1A9C0 in "_incObj/86 FZ Plasma Ball Launcher.asm", we can see this: Code (Text): move.w obX(a0),d0 sub.w $30(a0),d0 bcc.s loc_1A9E6 clr.w obVelX(a0) add.w d0,obX(a0) ; <-- BUG The first 2 lines get the distance between the plasma ball's current X position and its target X position. If it has moved past that target X position, that distance value should be negative. The branch after checks if the result underflowed (has gone negative), and if not, it skips over the rest. If it HAS, then it stops its movement and attempts to align it to the target X position by nudging it by the amount that it has moved past. The bug in question is that it uses an addition to do that alignment. When you add a negative number, it's a subtraction. In this case, it's basically moving the plasma ball MORE to the left, instead of pushing it towards the right to its actual target X position. To fix that, just change the add to a sub. Now, if you're okay with the balls spreading out a bit less like this, you can stop here. In fact, this is the same behavior as the 2013 Taxman remake, because it doesn't have that bug in it. But, if you want them to actually spread out further, then go to Obj86_Loop and change Code (Text): muls.w #-$4F,d1 to Code (Text): muls.w #-$59,d1 and you'll get this again. Spoiler: Bonus 2013 Taxman remake fix If you want to have the plasma balls spread out further without adding this bug, then in "SBZ/FZEggman.txt", in the FZEGGMAN_SETUP_PLASMAATTACK case, change the subtraction of 0x4F0000 to 0x590000.
The trap doors in Scrap Brain Zone have a small bug where if you are off screen, but not enough to despawn them, their sprite will wrap over and appear at the edge of the screen. For example, here's this trap door And if I move a bit to the left, but not have it despawned yet... The reason for this is that it has a pretty ridiculously high sprite width set on initialization. Code (Text): move.b #$80,obActWid(a0) The trap door sprite is $80 pixels wide, but the value set here is supposed to be half of that. The fix is simple: change the $80 to a $40. It should be noted that the sound for when it moves is only ever played when the object is on screen. Due to the larger width, the sound could play even with it was just slightly off screen, but with this change, it will no longer play if the object is not off screen at all. If you want to retain that behavior, then you'll have to manually check the camera's position.
So... wonder why you can't die at all in debug mode? It turns out the culprit lies in the line of code pointed with the arrow: Code (Text): HIVEBRAIN 2005: HurtSonic: tst.b ($FFFFFE2C).w ; does Sonic have a shield? bne.s Hurt_Shield ; if yes, branch tst.w ($FFFFFE20).w ; does Sonic have any rings? beq.w Hurt_NoRings ; if not, branch <--- This line jsr SingleObjLoad bne.s Hurt_Shield move.b #$37,0(a1) ; load bouncing multi rings object move.w 8(a0),8(a1) move.w $C(a0),$C(a1) Code (Text): GITHUB: HurtSonic: tst.b (v_shield).w ; does Sonic have a shield? bne.s @hasshield ; if yes, branch tst.w (v_rings).w ; does Sonic have any rings? beq.w @norings ; if not, branch <--- This line The code for that particular routine just has a check for debug mode and branching to the shield injury routine: Code (Text): HIVEBRAIN 2005: Hurt_NoRings: tst.w ($FFFFFFFA).w ; is debug mode cheat on? bne.w Hurt_Shield ; if yes, branch ; End of function HurtSonic Code (Text): GITHUB: @norings: tst.w (f_debugmode).w ; is debug mode cheat on? bne.w @hasshield ; if yes, branch So in HurtSonic, if you replace the pointed branch to Hurt_NoRings/@norings with KillSonic, the lack of rings properly kills Sonic regardless of being in debug mode or not. It's odd that no revision of Sonic 1 ever noticed this, much less any rom hacks of the game.
While this is fine and dandy, I don't think it's a bug. Having the debug mode disable dying makes sense, at least to me it does.
Same thing in Sonic CD. The function playdieset() is responsible for setting the necessary values if Sonic has to die. The first few lines check if edit mode is on, and if so, it immediately bails: Code (Text): if (editmode.w) return -1; But that's not all. The PC port also has an actual debug mode. In playdamage(), if you have no rings, you get damaged instead of dying if debug mode is on: Code (Text): if (debugflag.w) { playdamagechk(pActwk, pColliAct); return -1; } return playdieset(pActwk);
Yeah I'm guessing it'd be more of a consistency thing for how S2 and S3K handle deaths while in debug mode, ain't worth losin' sleep over it for sure
As I recall, it is a cheat on its own. In the Japanese version, you can enable four different cheats with four different combinations of U D L R (press C zero times, two times, four times, and six times). In the American version, it just enables everything. But I do think it's be nice if he could actually die if for no other reason than to restart the level, which is a legitimate reason to *not* have invulnerability. I like how they handled cheating death in the later games.
I've noticed that with this code tweak applied, the player can't go into debug mode to recover from a death, which definitely adds to their idea of cheating death in said mode (although not exact but oh well).
The background scrolling code for GHZ calculates the VScroll values in a way that doesn't make sense. The code below is from Revision 1, but Revision 0 is similar. Code (ASM): ; code from Hivebrain 2022 ; calculate Y position lea (v_hscroll_buffer).w,a1 move.w (v_camera_y_pos).w,d0 ; get camera pos andi.w #$7FF,d0 ; maximum $7FF lsr.w #5,d0 ; divide by $20 neg.w d0 addi.w #$20,d0 bpl.s .limitY ; branch if v_camera_y_pos is between 0 and $400 moveq #0,d0 ; use 0 if greater .limitY: move.w d0,d4 ; used later to determine where to start writing values for highest row of clouds move.w d0,(v_bg_y_pos_vsram).w ; update bg y pos The VScroll value varies inversely in relation to the camera y-pos: the maximum VScroll value of $20 is applied if the y-pos is 0, eventually falling to 0 once the camera reaches $400. This has the effect of the background scrolling in the "wrong" direction in relation to the camera, moving down as the camera rises, and up as the camera falls. While this may have been a deliberate design choice, I don't think it looks right. The following code makes the background scroll in the "correct" direction: Code (ASM): ; calculate Y position lea (v_hscroll_buffer).w,a1 move.w (v_camera_y_pos).w,d0 ; get camera pos andi.w #$7FF,d0 ; maximum $7FF lsr.w #5,d0 ; divide by $20 cmpi.w #$20,d0 bls.s .limitY ; branch if v_camera_y_pos is between 0 and $400 moveq #$20,d0 ; use $20 if greater .limitY: move.w d0,d4 move.w d0,(v_bg_y_pos_vsram).w ; update bg y pos
Avoiding CalcAngle When Performing Collision in the Air The way Sonic handles collision while he is airborne involves calculating the general direction that he is moving, and then jumping to the appropriate code for handling collision for that specific direction. If he is moving downwards, he would check collision with the walls and the floor. If he is moving left or right, he would check collision with the wall he is moving towards and the ceiling and floor. If he is moving upwards, he would check collision with the walls and ceiling. The way it checks that direction is to take the angle he is moving at by using CalcAngle with his X and Y speeds as the parameters, and the output angle value determines the direction he is moving. That works and all, but we can definitely use a less intensive calculation to handle this. What we can do is actually just directly check the speed values ourselves. What we can then deduce from this is that we can basically determine Sonic's direction depending on if he is moving more horizontally than vertically and the direction of the larger speed value. We know this, because the angle values that separate the different directions are all angles where the absolute values of X and Y are the same. Here is a chart showing the new logic with the conditions that should be checked: Here's an example implementation. Replace: Code (Text): Sonic_Floor: move.w obVelX(a0),d1 move.w obVelY(a0),d2 jsr (CalcAngle).l move.b d0,(v_unused3).w subi.b #$20,d0 move.b d0,(v_unused4).w andi.b #$C0,d0 move.b d0,(v_unused5).w cmpi.b #$40,d0 beq.w loc_13680 cmpi.b #$80,d0 beq.w loc_136E2 cmpi.b #$C0,d0 beq.w loc_1373E with: Code (Text): Sonic_Floor: move.w obVelX(a0),d0 ; Get X speed move.w obVelY(a0),d1 ; Get Y speed bpl.s SonAirCol_PosY ; If it's positive, branch cmp.w d0,d1 ; Are we moving towards the left? bgt.w loc_13680 ; If so, branch neg.w d0 ; Are we moving towards the right? cmp.w d0,d1 bge.w loc_1373E ; If so, branch bra.w loc_136E2 ; We are moving upwards SonAirCol_PosY: cmp.w d0,d1 ; Are we moving towards the right? blt.w loc_1373E ; If so, branch neg.w d0 ; Are we moving towards the left? cmp.w d0,d1 ble.w loc_13680 ; If so, branch
Full Position Alignment With Block Collision The way stage block collision works is that a function is called to determine the distance away or inside a block. Typically, if an object is inside an block, their speed is set to 0 and their position is aligned against the block. However, only the integer half of the position value tends to be affected, but not the fraction half. For example, let's say your Y position is 38.75, and you are inside a block 6 pixels deep downwards. The alignment would subtract your Y position by 6 pixels upwards, and your final Y position would end up being 32.75. In most cases, not clearing out the fraction part doesn't really seem to affect anything, but can still creep in some edge cases when it comes to movement on the ground, and mess up the collision. So, if you so desire to do a full alignment where the fraction part is cleared out (in the previous example, the final Y position ends up just being 32), it's rather easy: just search for the calls for the various collision detection functions, and in the part where the position value is aligned, also clear out the fraction part. For example: Code (Text): loc_13680: bsr.w Sonic_HitWall tst.w d1 bpl.s loc_1369A sub.w d1,obX(a0) clr.w obX+2(a0) ; <-- Add this move.w #0,obVelX(a0) move.w obVelY(a0),obInertia(a0) rts The functions to look for are Sonic_HitFloor, Sonic_HitWall, Sonic_DontRunOnWalls, sub_14EB4, ObjFloorDist, ObjFloorDist2, ObjHitWallRight, ObjHitCeiling, ObjHitWallLeft, and Sonic_WalkSpeed. Also FindFloor and FindWall calls throughout Sonic_AnglePos. Also, it might be worth capping Sonic's top ground speed at something like 15.75 ($FC0) in both directions and replace the rolling speed cap (that is only done to the X speed) to prevent further collision errors.
Here's a fun little oddity. The rolling speed cap only applies horizontally, but not vertically. If you don't want that, and instead would rather have it be consistent with the regular ground movement speed cap, just go to Sonic_RollSpeed, and change: Code (Text): move.b obAngle(a0),d0 jsr (CalcSine).l muls.w obInertia(a0),d0 asr.l #8,d0 move.w d0,obVelY(a0) muls.w obInertia(a0),d1 asr.l #8,d1 cmpi.w #$1000,d1 ble.s loc_131F0 move.w #$1000,d1 loc_131F0: cmpi.w #-$1000,d1 bge.s loc_131FA move.w #-$1000,d1 loc_131FA: move.w d1,obVelX(a0) to: Code (Text): move.b obAngle(a0),d0 jsr (CalcSine).l move.w obInertia(a0),d2 cmpi.w #$1000,d2 ble.s loc_131F0 move.w #$1000,d2 loc_131F0: cmpi.w #-$1000,d2 bge.s loc_131FA move.w #-$1000,d2 loc_131FA: muls.w d2,d0 asr.l #8,d0 move.w d0,obVelY(a0) muls.w d2,d1 asr.l #8,d1 move.w d1,obVelX(a0)
This is a slight tangent from the norm, but... ...is anyone documenting these changes segregated from the thread? Could we get some of them put on the wiki? Specifically here. Perhaps as a solution, we could put links on that wiki page to the fixes to specific posts of this thread, rather than creating a brand new wiki-page? Temporary measure of course. as the only issue I can see is if someone happens to have a conscientious mental breakdown or some form of insecurity, and goes back and edits their posts to replace the fixes (which happens from time to time), but it's better than nothing... at least as a temporary measure? If no-one wants to do it, then I'll try and find time to do a crude copy/paste set of pages maybe.
I'll be glad to help out as well, especially with the Sonic 2 thread as well. Both threads have a lot of great guides and such that'll benefit being sourced to a much wider audience with SCHG being a prime example to ROM hacking. I also want to rework some of the guides to be a little more user friendly and compatible with newer disassemblies as well, like the Sonic 2 level select port to Sonic 1 that's very dated nowadays and is hard for a newbie to get a good grasp at how everything works.
How about another guide? Properly Removing the Roll Jump Movement Lock So, some of you of course know about how when you jump after rolling, your movement is locked. Removing that lock is as simple as removing this line that's in Sonic_Jump: Code (Text): bset #4,obStatus(a0) However, there's one issue that needs to be addressed. You see, when you hold left or right in the air when your control isn't locked, there's a speed cap that gets applied. When the lock is active, moving left or right is prevented, which in turn, prevents that speed cap from kicking in. With that, you can jump and keep your momentum, if you are rolling down a hill, for instance: But, when we remove the lock and allow the speed cap to kick in... Yeaaaah, that's not ideal. Luckily, there's already a guide that makes it so that if you are moving faster than the maximum speed, it doesn't cap it (referred to as "removing the speed cap"). Apply those changes, and you'll be able to maintain your momentum and also be able to control yourself in the air.
This is a fix specifically for old Hivebrain version Project Sonic 1: Two-Eight. If you look at Sonic_Loops (Which also handles Sonic's interaction with the S-Tubes in Green Hill) you'll see: Code (Text): cmp.b #$75,d1 ; MJ: is the chunk 75 (Top top left S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch cmp.b #$76,d1 ; MJ: is the chunk 76 (Top top right S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch cmp.b #$77,d1 ; MJ: is the chunk 77 (Top bottom left S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch cmp.b #$78,d1 ; MJ: is the chunk 78 (Top bottom right S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch cmp.b #$79,d1 ; MJ: is the chunk 79 (Bottom top left S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch cmp.b #$7A,d1 ; MJ: is the chunk 7A (Bottom top right S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch cmp.b #$7B,d1 ; MJ: is the chunk 7B (Bottom bottom left S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch cmp.b #$7C,d1 ; MJ: is the chunk 7C (Bottom bottom right S Bend) beq.w Obj01_ChkRoll ; MJ: if so, branch Now, MarkeyJester, great guy, but what the hell went on here. Let's clean this up. We'll do 2 approaches. The first approach is just porting over the code from the P128 branch of the GitHub disassembly, this method is more modular, as it uses a table of chunks which you can edit if you move around the S-Tube IDs or make more S-Tubes. Just replace the above code with: Code (Text): lea STunnel_Chunks_End(pc),a2 ; MJ: lead list of S-Tunnel chunks moveq #(STunnel_Chunks_End-STunnel_Chunks)-1,d2 ; MJ: get size of list @loop: cmp.b -(a2),d1 ; MJ: is the chunk an S-Tunnel chunk? dbeq d2,@loop ; MJ: check for each listed S-Tunnel chunk beq.w Obj01_ChkRoll ; MJ: if so, branch Then, just before the " ; End of function Sonic_Loops" comment, insert this table: Code (Text): STunnel_Chunks: ; MJ: list of S-Tunnel chunks dc.b $75,$76,$77,$78 dc.b $79,$7A,$7B,$7C STunnel_Chunks_End: As I said this method is great because you can edit the table to any length with any chunk IDs. But it is flawed in that in execution, it's essentially the same as the original code, just condensed, it still loops through and checks each individual ID. And things could get nasty if you make every chunk an S-Tube chunk (Why would you do that). This is where the second approach comes in. If you know that your S-Tube chunks are only going to be within a specific ID range, then we only need to do 2 checks to see if we're within the S-Tube chunk range. Which can be achieved by replacing the original code with this: Code (Text): cmp.b #$75,d1 blo.s @NotInRange ; If the chunk ID is below our range, don't roll. cmp.b #$7C,d1 bhi.s @NotInRange ; If the chunk ID is above our range, don't roll. bra.w Obj01_ChkRoll ; Otherwise, if the chunk is in range, roll! @NotInRange: Now there is a third option that triumphs both of these, and this is the method Sonic 2 used, that is to make a dedicated force spin trigger object, this takes a lot more work which is out of the scope of this guide, though. But do look into it if the way P128 handles S-Tubes bothers you like it does to me.
So the steps to change the assembler options kind of bugged me for a while now, it's very hacky. And it wasn't compatible out of the box with the 2005 disasm since it has a single instance of vdp_control_port which is a very minor thing that takes a second to fix. But for the sake of convenience, here's a version that is 100% compatible with ASM68k and works with Hive 2005 by using the VDP control port's raw address. It allows you to skip changing the assembler options and declaring VDP_Command_Buffer and VDP_Command_Buffer_Slot as I put those in the file too.
Cleaning Up the Sound Driver if You've Fixed the Sega Chant This guide only effects Hivebrain 2005 users who are still using the default sound driver, as newer disassemblies don't face this issue. It's pretty infamous that when hacking with this disassembly you'll quickly run into a bug that causes the Sega chant to make a glitching noise towards the end of playback. A fix has been available for a long time now, and has been everyone's go to solution. What this fix does is use the 68k to play the chant rather than the Z80, because the Z80 sound driver expects the Sega chant sample to be at a specific ROM location and at a specific size. Not to mention it's Kosinski compressed, and the combination of these factors lead to this monstrosity when it comes time to include the Z80 driver: Code (Text): Kos_Z80: incbin sound\z80_1.bin dc.w ((SegaPCM&$FF)<<8)+((SegaPCM&$FF00)>>8) dc.b $21 dc.w (((EndOfRom-SegaPCM)&$FF)<<8)+(((EndOfRom-SegaPCM)&$FF00)>>8) incbin sound\z80_2.bin even But with the fix linked above, it doesn't need to be like this, as it's effectively dead code. So let's clean it up. You'll need Puto's sound driver disassembly which can be found here. First, if you plan on integrating this disassembly into your hack's disassembly then it's not a bad idea to delete all these equates since we won't be needing them. Code (Text): SEGA_Size equ 6978h ; The size of the SEGA sound SEGA_Location equ 9688h ; The location within the bank of the SEGA sound, 8000h-based SEGA_Pitch equ 0Bh ; The pitch of the SEGA sound Note that you do need SEGA_Bank and SEGA_Bank_Minor I tried deleting that and got my ears deleted instead. Then at Wait_for_DAC_Request you can delete Code (Text): cp 6 ; is the value=6 (playing sample 87)? jr nc,Play_SegaPCM ; If sample>=87, branch to Play_SegaPCM And finally you can delete the entire function Play_SegaPCM Code (Text): ; ; Subroutine - Play_SegaPCM ; ; This subroutine plays the "SEGA" sound. ; Play_SegaPCM: ld de,SEGA_Location ; Load the location of the SEGA sound (80h-based relative pointer to $78000 in the main ROM) to address de. ld hl,SEGA_Size ; Load the size of the SEGA sound to register hl. ld c,2Ah ; c = 2Ah PlaySegaPCM_Loop: ld a,(de) ; Load the contents of whatever de points at to register a ld (ix+0),c ; 4000h = c ld (ix+1),a ; 4001h = a ld b,SEGA_Pitch ; b = 0Bh (Pitch of the SEGA sample) loop_CA: djnz loop_CA ; Decrement b; jump to loop_CA if not 0. (Read the loop_8E comment for more info). inc de ; Increment de dec hl ; Decrement hl ld a,l ; a = l or h ; a = a or h jp nz,PlaySegaPCM_Loop ; If a!=0, jump to PlaySegaPCM_Loop jp Load_Sample ; Otherwise, if you finished playing the SEGA sound, jump back to Load_Sample Build and you'll have a cleaned up sound driver! Bring smpsbuilt_compressed.bin to your hack's main disassembly, rename it to z80 if you'd like or DAC or whatever, and replace kos_z80's nasty hack with a simple incbin.