don't click here

Some changes and fixes for Sonic 2

Discussion in 'Engineering & Reverse Engineering' started by Esrael, Jun 7, 2012.

  1. I've noticed that certain palette indexes are crashing the game; either I've discovered a new bug, or my ROM hack is trying to fight back against me; whatever the case, here's the index, in case someone figures out why the issue occurs.
    Code (Text):
    1. PalPointers:
    2. PalPtr_SEGA:    palptr Pal_SEGA,  0
    3. PalPtr_Title:    palptr Pal_Title, 1
    4. PalPtr_BGND:    palptr Pal_BGND,  0
    5. PalPtr_EHZ:    palptr Pal_EHZ,   1
    6. PalPtr_EHZ2:    palptr Pal_EHZ2,  1
    7. PalPtr_OWZ1:    palptr Pal_OWZ1,  1
    8. PalPtr_OWZ2:    palptr Pal_OWZ2,  1
    9. PalPtr_WZ:    palptr Pal_WZ,    1
    10. PalPtr_SSZ1:    palptr Pal_SSZ1,  1
    11. PalPtr_SSZ2:    palptr Pal_SSZ2,  1
    12. PalPtr_MTZ:    palptr Pal_MTZ,   1
    13. PalPtr_MTZ2:    palptr Pal_MTZ,   1
    14. PalPtr_WFZ:    palptr Pal_WFZ,   1
    15. PalPtr_HTZ:    palptr Pal_HTZ,   1
    16. PalPtr_HPZ:    palptr Pal_HPZ,   1
    17. PalPtr_RWZ:    palptr Pal_RWZ,   1
    18. PalPtr_OOZ:    palptr Pal_OOZ,   1
    19. PalPtr_MCZ:    palptr Pal_MCZ,   1
    20. PalPtr_CNZ:    palptr Pal_CNZ,   1
    21. PalPtr_CPZ:    palptr Pal_CPZ,   1
    22. PalPtr_CPZ2:    palptr Pal_CPZ2,   1
    23. PalPtr_DEZ:    palptr Pal_DEZ,   1
    24. PalPtr_ARZ:    palptr Pal_ARZ,   1
    25. PalPtr_ARZ2:    palptr Pal_ARZ2,   1
    26.         palptr Pal_EHZ,   1    ; for some reason, this specific entry crashes the game...
    27. PalPtr_SCZ:    palptr Pal_SCZ,   1
    28. PalPtr_OWZ1_U:    palptr Pal_OWZ1_U, 1
    29. PalPtr_HPZ_U:    palptr Pal_HPZ_U, 0
    30. PalPtr_CPZ_U:    palptr Pal_CPZ_U, 0
    31. PalPtr_SS:    palptr Pal_SS,    0
    32. PalPtr_ARZ_U:    palptr Pal_ARZ_U, 0
    33. PalPtr_ARZ2_U:    palptr Pal_ARZ2_U, 0
    34. PalPtr_MCZ_B:    palptr Pal_MCZ_B, 1
    35. PalPtr_CNZ_B:    palptr Pal_CNZ_B, 1
    36. PalPtr_SS1:    palptr Pal_SS1,   3
    37. PalPtr_SS2:    palptr Pal_SS2,   3
    38. PalPtr_SS3:    palptr Pal_SS3,   3
    39. PalPtr_SS4:    palptr Pal_SS4,   3
    40. PalPtr_SS5:    palptr Pal_SS5,   3
    41. PalPtr_SS6:    palptr Pal_SS6,   3
    42. PalPtr_SS7:    palptr Pal_SS7,   3
    43. PalPtr_SS1_2p:    palptr Pal_SS1_2p,3
    44. PalPtr_SS2_2p:    palptr Pal_SS2_2p,3
    45. PalPtr_SS3_2p:    palptr Pal_SS3_2p,3
    46. PalPtr_OOZ_B:    palptr Pal_OOZ_B, 1
    47. PalPtr_Result:    palptr Pal_Result,0
    48.         palptr Pal_EHZ,   1    ; this too!
    49.         palptr Pal_EHZ,   1    ; this too!
    50.         palptr Pal_EHZ,   1    ; this too!
    51. PalPtr_Knux:    palptr Pal_Knux,  0
    52. PalPtr_CPZ_K_U:    palptr Pal_CPZ_K_U, 0
    53. PalPtr_ARZ_K_U:    palptr Pal_ARZ_K_U, 0
    54. PalPtr_SS_K:    palptr Pal_SS_K, 0
    55. PalPtr_HPZ_K_U:    palptr Pal_HPZ_K_U, 0
    56. ; MENUS GO HERE
    57. PalPtr_SonicMenu:    palptr Pal_SonicMenu,  0
    58. PalPtr_TailsMenu:    palptr Pal_TailsMenu,  0
    59. PalPtr_KnuxMenu:    palptr Pal_KnuxMenu,  0
    60. PalPtr_EggmanMenu:    palptr Pal_EggmanMenu,  0
    61. PalPtr_RetroMenu:    palptr Pal_RetroMenu,  0
    62. PalPtr_Ending:    palptr    Pal_Ending, 0
     
  2. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,245
    1,415
    93
    your mom
    I'm not sure what you exactly mean by "crashing the game". Are you trying to load those Pal_EHZ entries? The only issue that I came across was with "levartptrs" not being able to take into account any additional operators without surrounding it with parenthesis (i.e. you can't put in PalID_EHZ+1, because it gets assembled as "PalID_EHZ+1<<24", which is PalID_EHZ+$1000000). To fix that alongside any other potential issues, I changed:
    Code (Text):
    1.     dc.l (plc1<<24)|art
    2.     dc.l (plc2<<24)|map16x16
    3.     dc.l (palette<<24)|map128x128
    to
    Code (Text):
    1.     dc.l ((plc1)<<24)|(art)
    2.     dc.l ((plc2)<<24)|(map16x16)
    3.     dc.l ((palette)<<24)|(map128x128)
     
    Last edited: Oct 14, 2022
  3. OrionNavattan

    OrionNavattan

    Tech Member
    165
    164
    43
    Oregon
    I assume "crashing the game" refers to a 68K exception occurring when you try to use those palette indices, but without knowing what kind of exception (address error, illegal instruction, etc.) is occurring, I would not know where to start looking. Are you using an exception handler in your hack?
     
  4. No, as in it crashes if ANYTHING tries to load those entries.
    I wasn't, although that gave me the idea to import the Sonic 1 error exception to see what comes up; I got Line 1111 Emulator $00000001 as my result.
     
  5. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,245
    1,415
    93
    your mom
    Well, how exactly are you loading those entries?
     
  6. OrionNavattan

    OrionNavattan

    Tech Member
    165
    164
    43
    Oregon
    Line 1111 Emulator exception at offset $00000001 (part of the initial stack pointer value in the vector table). Seems something is causing those pointers to jump to invalid code instead of their intended destination.
     
    Last edited: Oct 14, 2022
  7. Strange, as the broken entries are broken regardless of what's there, even with a generic EHZ entry.
    PalLoad function.
     
  8. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,245
    1,415
    93
    your mom
    What's the exact ID that you are passing to the function?
     
  9. The entry right before Knuckles' palette entry.
     
  10. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,245
    1,415
    93
    your mom
    Please post some code/be more specific, because otherwise, I'm lost. I wanna see how EXACTLY you are doing it. It's possible that there's something about it you are not catching. So far, you've only really told me what you are intending to do, but not what's actually being done.
     
    Last edited: Oct 15, 2022
  11. Code (Text):
    1.     moveq    #$36,d0    ; load Knuckles' palette index
    2. +    bsr.w    PalLoad_Now    ; load Sonic's palette line
    The code used to load the palette; and yes, I did also use move.b and even move.w to load it instead, but it still crashes.
     
  12. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,245
    1,415
    93
    your mom
    Okay, so a couple of things:
    Code (Text):
    1. PalPtr_SEGA:    palptr Pal_SEGA,  0            ; 00
    2. PalPtr_Title:    palptr Pal_Title, 1            ; 01
    3. PalPtr_BGND:    palptr Pal_BGND,  0            ; 02
    4. PalPtr_EHZ:    palptr Pal_EHZ,   1            ; 03
    5. PalPtr_EHZ2:    palptr Pal_EHZ2,  1            ; 04
    6. PalPtr_OWZ1:    palptr Pal_OWZ1,  1            ; 05
    7. PalPtr_OWZ2:    palptr Pal_OWZ2,  1            ; 06
    8. PalPtr_WZ:    palptr Pal_WZ,    1            ; 07
    9. PalPtr_SSZ1:    palptr Pal_SSZ1,  1            ; 08
    10. PalPtr_SSZ2:    palptr Pal_SSZ2,  1            ; 09
    11. PalPtr_MTZ:    palptr Pal_MTZ,   1            ; 0A
    12. PalPtr_MTZ2:    palptr Pal_MTZ,   1            ; 0B
    13. PalPtr_WFZ:    palptr Pal_WFZ,   1            ; 0C
    14. PalPtr_HTZ:    palptr Pal_HTZ,   1            ; 0D
    15. PalPtr_HPZ:    palptr Pal_HPZ,   1            ; 0E
    16. PalPtr_RWZ:    palptr Pal_RWZ,   1            ; 0F
    17. PalPtr_OOZ:    palptr Pal_OOZ,   1            ; 10
    18. PalPtr_MCZ:    palptr Pal_MCZ,   1            ; 11
    19. PalPtr_CNZ:    palptr Pal_CNZ,   1            ; 12
    20. PalPtr_CPZ:    palptr Pal_CPZ,   1            ; 13
    21. PalPtr_CPZ2:    palptr Pal_CPZ2,   1            ; 14
    22. PalPtr_DEZ:    palptr Pal_DEZ,   1            ; 15
    23. PalPtr_ARZ:    palptr Pal_ARZ,   1            ; 16
    24. PalPtr_ARZ2:    palptr Pal_ARZ2,   1            ; 17
    25.         palptr Pal_EHZ,   1                ; 18
    26. PalPtr_SCZ:    palptr Pal_SCZ,   1            ; 19
    27. PalPtr_OWZ1_U:    palptr Pal_OWZ1_U, 1            ; 1A
    28. PalPtr_HPZ_U:    palptr Pal_HPZ_U, 0            ; 1B
    29. PalPtr_CPZ_U:    palptr Pal_CPZ_U, 0            ; 1C
    30. PalPtr_SS:    palptr Pal_SS,    0            ; 1D
    31. PalPtr_ARZ_U:    palptr Pal_ARZ_U, 0            ; 1E
    32. PalPtr_ARZ2_U:    palptr Pal_ARZ2_U, 0            ; 1F
    33. PalPtr_MCZ_B:    palptr Pal_MCZ_B, 1            ; 20
    34. PalPtr_CNZ_B:    palptr Pal_CNZ_B, 1            ; 21
    35. PalPtr_SS1:    palptr Pal_SS1,   3            ; 22
    36. PalPtr_SS2:    palptr Pal_SS2,   3            ; 23
    37. PalPtr_SS3:    palptr Pal_SS3,   3            ; 24
    38. PalPtr_SS4:    palptr Pal_SS4,   3            ; 25
    39. PalPtr_SS5:    palptr Pal_SS5,   3            ; 26
    40. PalPtr_SS6:    palptr Pal_SS6,   3            ; 27
    41. PalPtr_SS7:    palptr Pal_SS7,   3            ; 28
    42. PalPtr_SS1_2p:    palptr Pal_SS1_2p,3            ; 29
    43. PalPtr_SS2_2p:    palptr Pal_SS2_2p,3            ; 2A
    44. PalPtr_SS3_2p:    palptr Pal_SS3_2p,3            ; 2B
    45. PalPtr_OOZ_B:    palptr Pal_OOZ_B, 1            ; 2C
    46. PalPtr_Result:    palptr Pal_Result,0            ; 2D
    47.         palptr Pal_EHZ,   1                ; 2E
    48.         palptr Pal_EHZ,   1                ; 2F
    49.         palptr Pal_EHZ,   1                ; 30
    50. PalPtr_Knux:    palptr Pal_Knux,  0            ; 31
    51. PalPtr_CPZ_K_U:    palptr Pal_CPZ_K_U, 0        ; 32
    52. PalPtr_ARZ_K_U:    palptr Pal_ARZ_K_U, 0        ; 33
    53. PalPtr_SS_K:    palptr Pal_SS_K, 0            ; 34
    54. PalPtr_HPZ_K_U:    palptr Pal_HPZ_K_U, 0        ; 35
    55. PalPtr_SonicMenu:    palptr Pal_SonicMenu,  0        ; 36
    56. PalPtr_TailsMenu:    palptr Pal_TailsMenu,  0        ; 37
    57. PalPtr_KnuxMenu:    palptr Pal_KnuxMenu,  0        ; 38
    58. PalPtr_EggmanMenu:    palptr Pal_EggmanMenu,  0        ; 39
    59. PalPtr_RetroMenu:    palptr Pal_RetroMenu,  0        ; 3A
    60. PalPtr_Ending:    palptr    Pal_Ending, 0        ; 3B
    Looks like you miscounted, or you changed the palette table.

    One more question, from where is this being run at? Is it from an object?
     
  13. It's just the routine that loads Sonic's (or, in this case, Knuckles') palette when starting the level (Level_LoadPal).
     
  14. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,245
    1,415
    93
    your mom
    Only thing I can think of is for you to go to where the stack pointer register points to in RAM, and follow the path of addresses that are pushed from calls/interrupts to determine where it's exactly crashing from, with the help of your listing output (note that interrupts/exceptions push the return address and the status register (which will look something like $23xx or $27xx in Sonic 2), so keep that in mind. Also if you are using vladikcomper's debugger, the stack trace starting from SP is already there at the bottom of the screen).

    Might be helpful to post the rest of what's under Level_LoadPal, maybe. It might be better to move this into Basic Q&A or a separate thread, to be honest. Kinda clogging up this thread.
     
  15. OrionNavattan

    OrionNavattan

    Tech Member
    165
    164
    43
    Oregon
    Fixing the Pitch of the Sega Sound

    This is one that I am absolutely amazed no one ANYWHERE has seemed to have noticed, much less discussed: the pitch of the SEGA Sound in Sonic 2 is just a tiny bit higher pitched than it should be. Whether it is intentional or not, I don't know, but I found it impossible to ignore. Thankfully, it is actually really easy to restore it to match Sonic 1 and 3K.

    In the sound driver, find this section under zPlaySegaSound:
    Code (Text):
    1.  
    2. .loop:
    3.    ld   a,(hl)           ; Get next PCM byte
    4.    ld   (zYM2612_D0),a       ; Send to DAC
    5.    inc   hl           ; Advance pointer
    6.    nop
    7.    ld   b,0Ch           ; Sega PCM pitch
    8.    djnz   $           ; Delay loop
    9.  
    10.    nop
    11.    ld   a,(zAbsVar.QueueToPlay)   ; Get next item to play
    12.    cp   c           ; Is it 80h?
    13.    jr   nz,.stop       ; If not, stop Sega PCM
    14.    ld   a,(hl)           ; Get next PCM byte
    15.    ld   (zYM2612_D0),a       ; Send to DAC
    16.    inc   hl           ; Advance pointer
    17.    nop
    18.    ld   b,0Ch           ; Sega PCM pitch
    19.    djnz   $           ; Delay loop
    20.  
    21.    nop
    22.    dec   de           ; 2 less bytes to play
    23.    ld   a,d           ; a = d
    24.    or   e           ; Is de zero?
    25.    jp   nz,.loop       ; If not, loop
    26.  
    Delete all four of the nops, and change both instances of '0Ch' to '0Dh'. That's it.
    (This does appear to be safe for real hardware; the 2612 in my Model 1 VA6 handles it without any problems.)



    Fix Erratic Behavior of Various Badniks in Debug Mode

    In debug mode, quite a few Badniks will still track Sonic while he is an object, while others will behave erratically, such as Grabbers moving up and down wildly. This is caused by Obj_GetOrientationToPlayer not checking for debug mode. This is also a simple fix:

    Code (Text):
    1. Obj_GetOrientationToPlayer:
    2.    moveq   #0,d0
    3.    moveq   #0,d1
    4.    lea   (MainCharacter).w,a1 ; a1=character
    5.    move.w   x_pos(a0),d2
    6.    sub.w   x_pos(a1),d2
    7.    mvabs.w   d2,d4   ; absolute horizontal distance to main character
    8.    lea   (Sidekick).w,a2 ; a2=character
    9.    move.w   x_pos(a0),d3
    10.    sub.w   x_pos(a2),d3
    11.    mvabs.w   d3,d5   ; absolute horizontal distance to sidekick
    12.    cmp.w   d5,d4   ; get shorter distance
    13.    bls.s   ++   ; branch, if main character is closer
    14.  
    Above the 'cmp.w d5,d4', insert these two lines:
    Code (Text):
    1.  
    2. tst.w   (Debug_placement_mode).w ; is debug mode active?
    3. bne.s    +       ; if so, treat sidekick as closer
    4.  
    There are quite a few other similar bugs in debug mode in both Sonic 1 and 2 that can be fixed by inserting those two lines in various locations.
     
    Last edited: Nov 13, 2022
  16. You know how in 2-player mode, whenever a teleport monitor is hit, the background color never changes? Well, turns out it's the result of a single line of code; in Vint_Level, you should see something similar to this:
    Code (Text):
    1. Vint_Level:
    2.     bsr.w    ReadJoypads
    3.     tst.b    (Teleport_timer).w
    4.     beq.s    loc_6F8
    5.     lea    (VDP_control_port).l,a5
    6.     tst.w    (Game_paused).w    ; is the game paused ?
    7.     bne.w    loc_748    ; if yes, branch
    8.     subq.b    #1,(Teleport_timer).w
    9.     bne.s    +
    10.     move.b    #0,(Teleport_flag).w
    11. +
    12.     cmpi.b    #$10,(Teleport_timer).w
    13.     blo.s    loc_6F8
    14.     lea    (VDP_data_port).l,a6
    15.     move.l    #vdpComm($0000,CRAM,WRITE),(VDP_control_port).l
    16.     move.w    #$EEE,d0
    17.  
    18.     move.w    #$1F,d1
    19. -    move.w    d0,(a6)
    20.     dbf    d1,-
    That $1F should be changed to $20, and now the color changes correctly.
     
  17. Clownacy

    Clownacy

    Tech Member
    1,060
    607
    93
    I don't think that's correct. Here's the complete code:

    Code (ASM):
    1.        lea    (VDP_data_port).l,a6
    2.        move.l    #vdpComm($0000,CRAM,WRITE),(VDP_control_port).l
    3.        move.w    #$EEE,d0 ; White.
    4.  
    5.        move.w    #32-1,d1
    6. -      move.w    d0,(a6)
    7.        dbf       d1,-
    8.  
    9.        ; Skip a colour.
    10.        move.l    #vdpComm($0042,CRAM,WRITE),(VDP_control_port).l
    11.  
    12.     if fixBugs
    13.        move.w    #31-1,d1
    14.     else
    15.        ; This does one more colour than necessary: it isn't accounting for
    16.        ; the colour that was skipped earlier!
    17.        move.w    #32-1,d1
    18.     endif
    19. -      move.w    d0,(a6)
    20.        dbf       d1,-
    What this does is fill the first two palette lines with white, skip the first colour of the third palette line, and then set the remaining colours to white. Skipping that one colour is deliberate. There is, however, a small bug where the first colour of the first palette line is set to white twice because of an incorrect loop counter.
     
  18. OrionNavattan

    OrionNavattan

    Tech Member
    165
    164
    43
    Oregon
    It's well-documented that the title cards on water levels in the game have a minor visual defect: they are drawn with the level's water palette for a single frame. A small issue, but no issue is too small to fix as far as I'm concerned.

    Stepping through the level load code with Exodus, the cause appears to be a race condition related to disabling interrupts while HBlank is enabled and set to run on scanline 224 (i.e., at the bottom of the screen). The latter normally causes HBlank and then VBlank to be run in that order (as evidenced by the different background color visible in the overscan at the bottom of the screen in CPZ2). However, if the end of a frame is reached while interrupts are disabled, things fall apart: the two interrupts are queued and processed after interrupts are reenabled, and as VBlank has a higher priority (6) than HBlank (4), it gets run first, causing the dry palette to be overwritten by the water palette when HBlank runs. DrawBlockRow/DrawRow (which is called multiple times by DrawInitalBG/DrawTilesAtStart) repeatedly disables and enables interrupts in order to write to VRAM, and the timings are just such that a frame ends while that subroutine has interrupts disabled.

    As far as I can tell, this race condition has the potential to occur anytime that interrupts are disabled AND HBlank is set to run at or near the bottom of the screen. The only way to truly eliminate it would be to disable HBlank if the water surface is not on-screen, but even that might not help if the water level is very close to the bottom. However, the instance that causes the miscolored title cards has a simple workaround: delay enabling HBlank on the VDP until we are ready to slide the title cards away.

    Under Level/GM_Level, find this:

    Code (ASM):
    1.  
    2. ; Sonic 2 GitHub
    3. +
    4.    move.w   (Hint_counter_reserve).w,(a6)
    5.    clr.w   (VDP_Command_Buffer).w
    6.    move.l   #VDP_Command_Buffer,(VDP_Command_Buffer_Slot).w
    7.    tst.b   (Water_flag).w   ; does level have water?
    8.    beq.s   Level_LoadPal   ; if not, branch
    9.    move.w   #$8014,(a6)   ; H-INT enabled
    10.  
    11. ;===========================
    12.  
    13. ; Sonic 2 ASM68K
    14.     .not_2P:
    15.        move.w   (v_vdp_hint_counter).w,(a6)
    16.        reset_dma_queue                   ; clear and reset the DMA queue
    17.        tst.b   (f_water).w               ; does level have water?
    18.        beq.s   .skip_water               ; if not, branch
    19.        move.w   #vdp_md_color|vdp_enable_hint,(a6)   ; normal color mode, horizontal interrupts enabled
    20.  
    21.  

    We want to move that last instruction all the way to near the end of the level load sequence. The best location would be immediately after the call to PalLoad_Water_ForFade/PalLoad_Water_Next:

    Code (ASM):
    1.  
    2. +
    3.    bsr.w   PalLoad_Water_ForFade
    4.    move.w   #$8014,(VDP_control_port).l
    5.  
    6. ;===========================
    7.  
    8.     .gotunderwaterpal:
    9.        bsr.w   PalLoad_Water_Next           ; load water palette that'll be shown after fading in
    10.        move.w   #vdp_md_color|vdp_enable_hint,(vdp_control_port).l ; normal color mode, horizontal interrupts enabled
    11.  

    Note that we have not changed how HBlank is enabled for two-player mode. It MUST be enabled where it is for things to work correctly in that case.
     
    Last edited: Apr 2, 2023
  19. Cool fix, although Sonic 2 Community Cut had a different way to resolve the issue by directly messing with PalToCRAM; for the sake of completeness, here it is (insert between Underwater_palette and vdpComm lines):
    Code (Text):
    1.     ; title card flicker fix from Community's Cut
    2.     move.l    d0,-(sp)
    3.     move.w    (Water_Level_1).w,d0
    4.     subi.w    #224,d0
    5.     cmp.w    (Camera_Y_pos).w,d0
    6.     ble.s    +
    7.     lea    (Normal_palette).w,a0     ; load palette from RAM
    8. +
    9.     move.l    (sp)+,d0
    Either works.
     
  20. OrionNavattan

    OrionNavattan

    Tech Member
    165
    164
    43
    Oregon
    The Rexon object has a near-completely implemented but unused feature in its code. Although it's hard to actually see it in action, its body normally oscillates back and forth in the lava while waiting for a player to get close. However, there is also code for a second behavior: simply standing still in the lava while waiting.

    Code (ASM):
    1.  
    2. ; Sonic 2 Git
    3. Obj94_ReadyToCreateHead:
    4.    bsr.w   Obj_GetOrientationToPlayer
    5.    addi.w   #$60,d2
    6.    cmpi.w   #$100,d2
    7.    bhs.s   loc_373AE
    8.    bsr.w   Obj94_CreateHead
    9.  
    10. loc_373AE:
    11.    bsr.w   Obj94_SolidCollision
    12.    jmpto   MarkObjGone, JmpTo39_MarkObjGone
    13.  
    14. ; ===========================================================================
    15. ; Sonic 2 ASM68K
    16. Rex_Wait_Stationary:       ; unused, as ost_routine is never set to run it ($4)
    17.        bsr.w   GetClosestPlayer       ; get nearest player
    18.        addi.w   #$60,d2
    19.        cmpi.w   #$100,d2
    20.        bcc.s   .no_spawn               ; branch if they're not close enough yet
    21.        bsr.w   Rex_SpawnHead
    22.  
    23.    .no_spawn:              
    24.        bsr.w   Rex_Solid
    25.        jmpto   DespawnObject,JmpTo39_DespawnObject
    26.  

    The only thing that's missing is code to actually set that routine. This cannot be done by subtype, since the Rexon uses the subobject data system, which uses the subtype OST slot to store the index into the subobject data pointer list. However, the fact that the Rexon has two pointers and two IDs ($94 and $96, with only the latter used) associated with it seems to suggest that the object ID would have been used to trigger this behavior. The Grounder object actually does this, with one ID signifying a Grounder hidden in a wall, the other a Grounder that's already roaming. Patterning off of that, finishing this unfinished feature is as simple as adding a few lines to Obj94_Init/Rex_Init:

    Code (ASM):
    1.  
    2. ; Sonic 2 Git
    3. Obj94_Init:
    4.    bsr.w   LoadSubObject
    5.    move.b   #2,mapping_frame(a0)
    6.    cmpi.b   #ObjID_Rexon,id(a0) ; $94 for stationary, $96 for moving
    7.    beq.s   .stationary                   ; branch if this Rexon is stationary
    8.    move.w   #-$20,x_vel(a0)
    9.    move.b   #$80,objoff_2A(a0)
    10.    rts
    11.  
    12.  .stationary:
    13.    move.b   #4,routine(a0) ; Obj94_ReadyToCreateHead
    14.    rts
    15.  
    16. ; ===========================================================================
    17. ; Sonic 2 ASM68K
    18. Rex_Init:              
    19.        bsr.w   LoadSubtypeData                       ; go to Rex_Wait next
    20.        move.b   #id_Frame_Rexon_Body,ost_frame(a0)
    21.        cmpi.b   #id_Rexon_Dup,ost_id(a0) ; $94 for stationary, $96 for moving
    22.        beq.s   .stationary                          ; branch if this Rexon is stationary
    23.        move.w   #-$20,ost_x_vel(a0)
    24.        move.b   #$80,ost_rex_turntime(a0)   ; move left for 128 frames
    25.        rts
    26.  
    27.   .stationary:
    28.       move.b   #id_Rex_Wait_Stationary,ost_primary_routine(a0)
    29.       rts
    30.