Fixing Sonic 2s' collapsing GHZ platforms

Discussion in 'Tutorials' started by Pacca, Jul 17, 2019.

  1. Pacca

    Pacca Why succeed when you can profit off of failure? Member

    Jul 5, 2014
    Triton (Moon)
    As many of you may already know, Sonic 2 has a couple of very interesting and rather useful unused objects leftover from Sonic 1, like the breakable walls and purple rocks. As it turns out, the collapsing platforms in Hidden Palace (Object 1A) are derived from Sonic 1s' collapsing slopes in Green Hill, and will act just like them in other zones (except for Oil Ocean, it's supported their but unused)! This is very convenient if your porting Green Hill Zone! If your anything like me, you might want to give them alternative graphics and place them in your own custom zones as well. Sadly, however, they suffer from a nasty issue when used as is:

    Try to ignore the loud background music X3

    They will clip you through floors! In the best of cases, this can be avoided by jumping at the right time, or it will push you into a lower path. But if used near the death barrier (something that actually happens in GHZ3), it can teleport you directly into it and instakill you for no good reason :eek:

    Now, I spent way too long tracking this down, so I have a short answer and a long one. Short answer; find "Obj1A_GHZ_SlopeData:". It should look like this:

    ; S1 remnant: Height data for GHZ collapsing platform (unused):
        dc.b $20,$20,$20,$20,$20,$20,$20,$20,$21,$21,$22,$22,$23,$23,$24,$24
        dc.b $25,$25,$26,$26,$27,$27,$28,$28,$29,$29,$2A,$2A,$2B,$2B,$2C,$2C; 16
        dc.b $2D,$2D,$2E,$2E,$2F,$2F,$30,$30,$30,$30,$30,$30,$30,$30,$30,$30; 32
    Copy paste this line underneath it:

        dc.b $30,$30,$30,$30,$30,$30,$30,$30,$30,$30,$30,$30,$30,$30,$30,$30; 48    ;dummy values, to prevent accidentally going out of bounds.
    And that's that.

    And now for the long answer, based on how I tracked the issue down! Because I want to justify being scared of this issue for so damn long :Y

    The first thing that I thought might cause the problem was the way the platform breaks, but even if you disable that (making it just a standard sloped platform), the issue still occurs. So I set about trying to learn how it collided with things, because object collision is a thing I've been rather fuzzy on.

    The meat of how the platform collides is in the SlopedPlatform routine, which is only used by this object and the seesaws. Notably, neither of these clip into the ground the way the Green Hill platforms do; I was initially worried that might be contributing to the issue. I also immediately found out that it had an input that wasn't being set; it apparently accepts the objects height in d3, at least according to the comments in the disassembly. Not only was d3 not being set by object 1A, but it wasn't even referenced anywhere in the objects code, so it was likely just dumping nonsense from whatever previous use d3 had into the routine, which could definitely lead to wacky behavior. Fixing that by setting it to a reasonable value did nothing to the issue though. I tried several different values and nothing noticeably changed...

    With my options pretty much exhausted regarding potential issues with calling the routine (everything seemed to be passed in properly, and there was no real collision code outside of that call), I got to work commenting the entire routine. Here it is, if your curious. I was able to follow along relatively well, even if I did get tripped up by some bitwise operations being used on numerical values...

    ; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
    ; input variables:
    ; d1 = object width
    ; d3 = object height
    ; d4 = object x-axis position
    ; address registers:
    ; a0 = the object to check collision with
    ; a1 = sonic or tails (set inside these subroutines)
    ; a2 = height data for slope
    ; loc_19C8A: SlopeObject:
        lea    (MainCharacter).w,a1 ; a1=character
        moveq    #p1_standing_bit,d6    ;saving standing bit in d6 (faster/smaller most likely)
        movem.l    d1-d4,-(sp)    ;store details in stack to check with tails
        bsr.s    SlopedPlatform_SingleCharacter    ;run actual SlopedPlatform code
        movem.l    (sp)+,d1-d4    ;restore details in stack to check for tails
        lea    (Sidekick).w,a1 ; a1=character
        addq.b    #1,d6        ;this presumably adjusts standing bit to deal with tails
    ; loc_19CA0:
        btst    d6,status(a0)        ;is this player already standing on us?
        beq.w    SlopedPlatform_cont    ;if not, this checks if player is on us and sets thier on object flag
        move.w    d1,d2        ;copy given object width to d2
        add.w    d2,d2            ;object width * 2
        btst    #1,status(a1)    ;is this player in the air?
        bne.s    loc_19CC4        ;if so, branch
        move.w    x_pos(a1),d0    ;save players x_pos in d0
        sub.w    x_pos(a0),d0    ;subtract our own x_pos from d0 (distance between object and player in x axis?)
        add.w    d1,d0            ;add object width to distance between player and object
        bmi.s    loc_19CC4    ;presumably, if player is no longer on the object width wise, make player fall off
        cmp.w    d2,d0        ;compare distance to width * 2 (presumably checking other side of object)
        blo.s    loc_19CD8    ;if player is indeed on the slope, run normal sloping code.
    ;this runs when player enters the air while on the platform.
    ;Clears the on platform flags for both us and the given player
        bclr    #3,status(a1)
        bset    #1,status(a1)
        bclr    d6,status(a0)
        moveq    #0,d4
    ; ---------------------------------------------------------------------------
        move.w    d4,d2
        bsr.w    MvSonicOnSlope    ;This subroutine is the likely culprit
        moveq    #0,d4
    ; ===========================================================================
    ; ===========================================================================
        btst    #3,status(a1)    ;is player standing on object?
        beq.s    return_19C0C    ;if not, rts
        move.w    x_pos(a1),d0    ;move players x_pos into d0
        sub.w    x_pos(a0),d0    ;subtract our x_pos (distance between us and object)
        add.w    d1,d0            ;add object width to distance (d1 stores object width from earlier)
        lsr.w    #1,d0            ;bitshift distance from far end of object to the right? (logical shift right, saves sign and makes it 'smaller')
        btst    #0,render_flags(a0)    ;is this object mirrored on x axis?
        beq.s    loc_19BEC        ;if not, branch
        not.w    d0            ;reverse distance value bitwise?
        add.w    d1,d0            ;add object width to it again (maybe to test other far side?)
        move.b    (a2,d0.w),d1    ;this gets a value from the given slope height map into d1 based on d0 (d0 is relative to player position on the object)
        ext.w    d1        ;extend byte value in d1 to a word value
        move.w    y_pos(a0),d0    ;copy our y_pos into d0
        sub.w    d1,d0                ;subtract value from heightmap from our y_pos
        moveq    #0,d1        ;clear d1
        move.b    y_radius(a1),d1    ;copy players y_radius into d1
        sub.w    d1,d0            ;subtract players y_rad from heightmap value relative to our position (ie where they "should" be)
        move.w    d0,y_pos(a1)    ;set players y_pos to appropriate position
        sub.w    x_pos(a0),d2        ;subtract player x_pos from our old position
        sub.w    d2,x_pos(a1)        ;subtract distance from our actual x_pos (?)
    I didn't find anything odd that would interact with the regular block based level ground in anyway. I know a lot about how the standing on object flag works, so I knew that it wasn't really able to clip the player into anything as quickly as this issue does, especially without lingering. I thought that it setting the in air flag while I was walking onto solid ground might be an issue, but that didn't have an effect either.

    So I copied the object into a clear area with no level based ground nearby so I could test it in isolation. And lo and behold, the issue happened even without nearby ground. But it got even weirder then that! With no nearby collision and it's collapsing feature disabled, I found I could walk on it even after it teleported me down! And from there I could walk back to teleport up, or walk a teensy bit more to the side to have a weirdly bumpy ride off of thin air! And that's when it hit me; the MvSonicOnSlope routine accepts collision data, but has no boundary checks! It just uses the players position relative to the object and the objects own width to determine which data to read for the slope!

    So I went to checkout the data itself. It looked perfectly fine at a glance, and it was also the same size as the data for the hidden palace zone variant of the platform (which obviously works just fine, no weirdness there). So I checked the object width of each variant. Hidden Palace uses a width of $30, but Green Hill uses a width of $34, despite having the same sized data set. This extra width made MvSonicOnSlope read the mappings data after it as being the last 4 bytes of slope, causing it to naturally allow the player to follow that buggy slope section well below the intended area! All that effort, just to find out Sonic Team got lazy! Guess that's what I get for using unused objects :p
    EMK-20218, maple_t, KCEXE and 7 others like this.