Tripping the Lock-On™ Technology

Discussion in 'Discussion & Q&A' started by RealMalachi, Jan 21, 2024.

  1. RealMalachi

    RealMalachi You look like an atheist seeing an angel... Member

    Joined:
    Oct 16, 2022
    Messages:
    18
    Location:
    Australia
    Sonic 3 history speedrun, it was rushed and got DLC to finish it; a common practice nowadays. That DLC, however, was forwards compatible with Sonic 2, with other games unlocking a seed-generated blue spheres level, aside for Sonic 1 which unlocked a near-endless game.
    I was researching exactly what Sonic & Knuckles needs for a locked-on games rom header, and found a flaw in how it detects Sonic 1, but only Sonic 1.

    This is the code detecting Sonic 1s serial numbers, annotated by me because S&Ks disassembly is a wild west of documentation
    Code:
    sub_4CCA6:
            lea    (BlueSpheresSerialsText).l,a1
            moveq    #4-1,d1        ; loop 4 valid serial
    .loop
            lea    (LockonSerialNumber).l,a0    ; load locked game serial location
            moveq    #0,d3                ; temporarily set as valid
            moveq    #13-1,d2            ; loop checking 13 bytes
    .loopserialcheck
            move.b    (a1)+,d0        ; get byte from valid serial
            cmp.b    (a0)+,d0        ; compare to locked game serial
            ;cmpm.b    (a0)+,(a1)+    ; ahem
            beq.s    +            ; if the same, don't set serial as invalid
            moveq    #1,d3            ; invalid
    +        dbf    d2,.loopserialcheck
    
            tst.b    d3            ; is serial a match with one of the valid ones?
            beq.s    .fullbs            ; if so, unlock full blue sphere
            dbf    d1,.loop
            move.b    #0,(Blue_spheres_mode).w
            move.b    #-1,(Blue_spheres_progress_flag).w
            bsr.s    sub_4CD18        ; this calculates a seed based on the locked games header
    .rts
            rts
    .fullbs
            move.b    #1,(Blue_spheres_mode).w
            tst.b    (Blue_spheres_menu_flag).w
            bne.s    .rts
            clr.b    (Blue_spheres_progress_flag).w
            move.l    #0,(Blue_spheres_current_level).w
            move.l    #$10203,(Blue_spheres_current_stage).w
            rts
    ; End of function sub_4CCA6
    ; ---------------------------------------------------------------------------
    BlueSpheresSerialsText:
            dc.b "GM 00001009-0"    ; REV00 (non-JP serial)
            dc.b "GM 00004049-0"    ; REV01 (JP serial)
    ; 2 serials
    ; ---------------------------------------------------------------------------
    What this code does is check 13 bytes in the locked games serial with a list of pre-defined valid serials, notably excluding the last 14th byte. The code, while not the best, gets the job done... except for the obvious issue of checking for four serials when it only defines two.
    Before we theorize why, what's after those valid serials anyway?
    Code:
    sub_4CD18:
            lea    (LockonSerialNumber).l,a1
            moveq    #$A,d1
    
    loc_4CD20:
            move.b    (a1),d0
            subi.b    #$30,d0
            beq.s    loc_4CD2E
            cmpi.b    #$A,d0
            blo.s    loc_4CD34
    
    loc_4CD2E:
            addq.w    #1,a1
            dbf    d1,loc_4CD20
    ...
    The code calculating the blue spheres seed for non-Sonic 1 games.
    Here's the valid data in full:
    Code:
        dc.b $43,$F9,$00,$20,$01,$80,$72,$0A,$10,$11,$04,$00,$00    ; S&K arbitrary serial 1
        dc.b $30,$67,$06,$0C,$00,$00,$0A,$65,$06,$52,$49,$51,$C9    ; S&K arbitrary serial 2
    So, why?
    Look at the code detecting Sonic 2 and Sonic 3 (which is better documented by default)
    Code:
    DetermineWhichGame:
            lea    (LockonSerialsText).l,a1
            moveq    #4-1,d1    ; 3 Sonic 2 headers, 1 Sonic 3 header
    
    $$compareSerials:
            lea    (LockonSerialNumber).l,a0
            moveq    #0,d3
            moveq    #14-1,d2
    
    $$compareChars:
            move.b    (a1)+,d0
            cmp.b    (a0)+,d0
            beq.s    $$matchingChar
            moveq    #1,d3
    
    $$matchingChar:
            dbf    d2,$$compareChars
            tst.b    d3
            beq.s    S2orS3LockedOn
            dbf    d1,$$compareSerials
            bra.s    BlueSpheresStartup
    ; ---------------------------------------------------------------------------
    
    S2orS3LockedOn:
            tst.w    d1
            beq.w    SonicAndKnucklesStartup
            move.b    #1,(SRAM_access_flag).l
            jmp    ($300000).l                ; May be changed at a later date to become compatible with S2K disassembly
    ; ---------------------------------------------------------------------------
    LockonSerialsText:
            dc.b "GM 00001051-00"    ; Sonic 2 REV00/1/2
            dc.b "GM 00001051-01"
            dc.b "GM 00001051-02"
            dc.b "GM MK-1079 -00"    ; Sonic 3
    The code is near identical to Sonic 1s detection, just with a different outcome for succeeded and failing, checking all 14 bytes, and actually having four serials to check
    Basically, I believe it was a copy/paste job.

    Now how is this useful? Basically, if you decide you want your game to unlock full blue spheres when locked onto Sonic and Knuckles, you don't need Sonic 1s serial number to do it. This can be useful in the rare case that an emulator treats Sonic 1 differently, like say, emulating the exact speed of the games rom, HD graphic packs, overclocking by default, or outright preventing it from loading. For homebrew games, it's also probably best to distance yourself from any proprietary Sega data (hasn't stopped like half of them from doing so anyway, but whatever).
    Might just be me, but there's also partially a technical flex to it, using a mistake from some dev in the 90s for a neat feature
     
    DeltaWooloo and ProjectFM like this.
  2. Devon

    Devon I'm a loser, baby, so why don't you kill me? Member

    Joined:
    Aug 26, 2013
    Messages:
    1,376
    Location:
    your mom
    Speaking of Lock-On Technology, VAdaPEGA recently found out that Knuckles in Sonic 2 directly calls Sonic 2's sound driver loading function directly from the Sonic 2 cartridge before patching the sound driver to fix sound data addresses. Basically, this means that you can set up a fake Sonic 2 header and an entry point where Sonic 2's sound driver loading routine is at to effectively allow any ROM to have an "& Knuckles" mode. Though, you will need to still be aware of the limitations of how cartridge ROMs get mapped alongside Sonic & Knuckles.

    I talked with him and he has given me permission to write up a guide on this sometime.

    EDIT
    : Bah, there's issues with SRAM, so this might not be the most reliable method.

    There is an alternate method though. When Sonic & 3 Knuckles boots up, it first decompresses the Sega logo into RAM. You can take advantage of this by setting up Kosinski data at the same address as that, in which it will clear out most of RAM, and then replace the return address for the call to whatever you want it to be.

    Though, if you want a more optimal set of data (839 ($347) bytes vs. 73151 ($11DBF) bytes), here's this:
    Code:
            dc.w    %0101010101010101
            dc.b    $00                     ; Write "00"
            dc.b    $FF, $F8, $F3           ; Copy "00" 244 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    
            rept    30
            dc.w    %0101010101010101
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            endr                            ; (Replace with "endm" if using AS)
    
            dc.w    %0101010111110101
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
            dc.b    $00, $00                ; Write "00 00"
    
            dc.w    %0010111100000000
            dc.b    $00                     ; Write "00"
            dc.l    CPU_EntryPoint_SNK      ; Write return address
            dc.b    $00, $F0, $00           ; End of data
            even
     
    Last edited: Jan 22, 2024
    DeltaWooloo, ProjectFM and Pacca like this.
  3. Pacca

    Pacca Having an online identity crisis since 2019 Member

    Joined:
    Jul 5, 2014
    Messages:
    1,175
    Location:
    Limbo
    I have always figured there would be a way to exploit lock-on to grab control and allow custom lock on behavior. Happy to see people who actually know what they're doing exploring the concept, could be fun :3