don't click here

Tripping the Lock-On™ Technology

Discussion in 'Engineering & Reverse Engineering' started by RealMalachi, Jan 21, 2024.

  1. RealMalachi

    RealMalachi

    you can call me mal Member
    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 (Text):
    1. sub_4CCA6:
    2.         lea    (BlueSpheresSerialsText).l,a1
    3.         moveq    #4-1,d1        ; loop 4 valid serial
    4. .loop
    5.         lea    (LockonSerialNumber).l,a0    ; load locked game serial location
    6.         moveq    #0,d3                ; temporarily set as valid
    7.         moveq    #13-1,d2            ; loop checking 13 bytes
    8. .loopserialcheck
    9.         move.b    (a1)+,d0        ; get byte from valid serial
    10.         cmp.b    (a0)+,d0        ; compare to locked game serial
    11.         ;cmpm.b    (a0)+,(a1)+    ; ahem
    12.         beq.s    +            ; if the same, don't set serial as invalid
    13.         moveq    #1,d3            ; invalid
    14. +        dbf    d2,.loopserialcheck
    15.  
    16.         tst.b    d3            ; is serial a match with one of the valid ones?
    17.         beq.s    .fullbs            ; if so, unlock full blue sphere
    18.         dbf    d1,.loop
    19.         move.b    #0,(Blue_spheres_mode).w
    20.         move.b    #-1,(Blue_spheres_progress_flag).w
    21.         bsr.s    sub_4CD18        ; this calculates a seed based on the locked games header
    22. .rts
    23.         rts
    24. .fullbs
    25.         move.b    #1,(Blue_spheres_mode).w
    26.         tst.b    (Blue_spheres_menu_flag).w
    27.         bne.s    .rts
    28.         clr.b    (Blue_spheres_progress_flag).w
    29.         move.l    #0,(Blue_spheres_current_level).w
    30.         move.l    #$10203,(Blue_spheres_current_stage).w
    31.         rts
    32. ; End of function sub_4CCA6
    33. ; ---------------------------------------------------------------------------
    34. BlueSpheresSerialsText:
    35.         dc.b "GM 00001009-0"    ; REV00 (non-JP serial)
    36.         dc.b "GM 00004049-0"    ; REV01 (JP serial)
    37. ; 2 serials
    38. ; ---------------------------------------------------------------------------
    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 (Text):
    1. sub_4CD18:
    2.         lea    (LockonSerialNumber).l,a1
    3.         moveq    #$A,d1
    4.  
    5. loc_4CD20:
    6.         move.b    (a1),d0
    7.         subi.b    #$30,d0
    8.         beq.s    loc_4CD2E
    9.         cmpi.b    #$A,d0
    10.         blo.s    loc_4CD34
    11.  
    12. loc_4CD2E:
    13.         addq.w    #1,a1
    14.         dbf    d1,loc_4CD20
    15. ...
    The code calculating the blue spheres seed for non-Sonic 1 games.
    Here's the valid data in full:
    Code (Text):
    1.     dc.b $43,$F9,$00,$20,$01,$80,$72,$0A,$10,$11,$04,$00,$00    ; S&K arbitrary serial 1
    2.     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 (Text):
    1. DetermineWhichGame:
    2.         lea    (LockonSerialsText).l,a1
    3.         moveq    #4-1,d1    ; 3 Sonic 2 headers, 1 Sonic 3 header
    4.  
    5. $$compareSerials:
    6.         lea    (LockonSerialNumber).l,a0
    7.         moveq    #0,d3
    8.         moveq    #14-1,d2
    9.  
    10. $$compareChars:
    11.         move.b    (a1)+,d0
    12.         cmp.b    (a0)+,d0
    13.         beq.s    $$matchingChar
    14.         moveq    #1,d3
    15.  
    16. $$matchingChar:
    17.         dbf    d2,$$compareChars
    18.         tst.b    d3
    19.         beq.s    S2orS3LockedOn
    20.         dbf    d1,$$compareSerials
    21.         bra.s    BlueSpheresStartup
    22. ; ---------------------------------------------------------------------------
    23.  
    24. S2orS3LockedOn:
    25.         tst.w    d1
    26.         beq.w    SonicAndKnucklesStartup
    27.         move.b    #1,(SRAM_access_flag).l
    28.         jmp    ($300000).l                ; May be changed at a later date to become compatible with S2K disassembly
    29. ; ---------------------------------------------------------------------------
    30. LockonSerialsText:
    31.         dc.b "GM 00001051-00"    ; Sonic 2 REV00/1/2
    32.         dc.b "GM 00001051-01"
    33.         dc.b "GM 00001051-02"
    34.         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
     
  2. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,261
    1,432
    93
    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.
     
  3. RealMalachi

    RealMalachi

    you can call me mal Member
    Gotta love it when people do things you genuinely thought was impossible with relative ease, bonus points when it was hiding in plain sight for around a decade. Now someone might be able to make KiS1 via lock-on real, or something much, much worse. Thank you for sharing this Devon.
     
  4. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    809
    203
    43
    Lyon, France
    Sonic 2 Long Version
    I actually found out how to trick S&K cartridge 12 years ago, and implemented it in my ROM hack. It works wonders on real hardware with an MDPro cartridge locked-on S&K.

    MDPro alone, not locked-on: Sonic 2 Long Version
    MDPro locked-on: Knuckles in Sonic 2 Long Version

    The idea is to trick S&K that a S3 cartridge is locked-on by providing a fake S3 header at $200000. Then, S3&K will boot and before displaying anything on screen, will try to load and decompress the first graphics of the S3 title screen by seeking them in the S3 cartridge at $350D26. Instead of the actual S3 graphics, you trick the game by providing a snapshot of RAM to decompress, and ultimately erases the stack where the return address is stored... Once the game finally decompressed everything, it reads the return address that you just erased, and jumps everywhere you want to. Just make it jump in the locked-on cartridge memory (let's say $200400), and you are almost done. Now, adjust H-Int and V-Int routines like this:

    Code (Text):
    1. LockOn_GameProgram: ; $200400
    2.     lea    (System_Stack).w,sp ; Reload stack pointer
    3.  
    4.     ; Load the code "jmp V_Int" to RAM
    5.     ; The V_Int and H_Int addresses are defined in RAM to allow for S&K to coexist with S2K.
    6.     move.w    #$4EF9,(V_Int_jump).w
    7.     move.l    #V_Int,(V_Int_jump_address).w
    8.     move.w    #$4EF9,(H_Int_jump).w
    9.     move.l    #H_Int,(H_Int_jump_address).w
    With this, the game will boot directly on the locked-on cartridge at address $200400. You can even make use of the SRAM if you trick S&K to make use of it. But that's another story.

    EDIT: I think my method is much more effective than the above. This way, you don't even have to activate and jump to the S2 UPMEM chip. If I remember right, once the S2 UPMEM chip is activated, there is no way to come back and shut it down. Meaning that addresses from $300000 to $340000 become unusable.

    EDIT2: An easy challenge is to build a 4Mb ROM hack with regular S1 at $000000, a hack of Knuckles in S1 with fake S3 header at $200000 and fake S3 title screen graphics at $350D26. Yep, full Knuckles in Sonic 1 cartridge lock-on.
     
    Last edited: Jan 21, 2024
  5. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,261
    1,432
    93
    your mom
    This is false. The KiS2 UPMEM is enabled and disabled alongside SRAM via the SRAM enable flag.
     
  6. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    809
    203
    43
    Lyon, France
    Sonic 2 Long Version
    The fact is, this is not really an SRAM enable flag. Writing 1 to $A130F1 only generates a pulse on pin 31. With the internal S&K circuitry in mind, this implies that every access above $300000 disables every chip (S&K and locked-on cartridge) besides the S2 UPMEM chip. Writing 0 to $A130F1 deactivates this feature: every access above $200000 disables the S&K ROM and enables the locked-on cartridge, but never enables the S2 UPMEM chip.

    This means that once you are executing 68000 code inside the S2 UPMEM chip, there is no way back. The S2 UPMEM chip never write 0 to $A130F1 because you are executing actual 68000 code on that chip. The S2 UPMEM chip is only deactivated (with internal circuitry) when seeking data in the S&K or S2 locked-on cartridge, or when executing the S2 Z80 sound driver.

    SRAM is another story. This is internal S3 circuitry that grants access to SRAM because the !RESET pin is always grounded by S&K cartridge. Generating a pulse on pin 31 (writing 1 to $A130F1) while !RESET is grounded activates the SRAM in S3 cartridge. Writing 0 to $A130F1 deactivates SRAM, granting access to the main S3 cartridge chip. This works because you are not executing 68000 code in the S2 UPMEM chip, but in the S&K chip. And this works because the S3 circuitry was built with S&K lock-on technology in mind.
     
    • Informative Informative x 1
    • List
  7. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,261
    1,432
    93
    your mom
    Well, my point was that you could use the fact that KiS2 runs code *directly* off of the Sonic 2 cartridge to effectively implement your own entry point at the same location that function is called at within your own cartridge ROM and begin running code off of that instead of continuing back into KiS2, and even write 0 to $A130F1 to return to the normal S&K + cartridge ROM mapping. The whole point is to *escape* the KiS2 UPMEM. Hell, the Z80 sound driver is patched so that the 68000 bank window is set to reference the Sonic 2 cartridge ROM. Obviously, running code and reading data from the locked on cartridge is no issue when running KiS2, so theoretically, this should work fine?
     
    Last edited: Jan 21, 2024
  8. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    809
    203
    43
    Lyon, France
    Sonic 2 Long Version
    Actually, you are right. This should work.
    I was sure that KiS2 never executed 68000 code outside of the S2 UPMEM chip. So I found a workaround 12 years ago (EDIT: No KiS2 disassembly was available at that time).

    But:

    Code (Text):
    1. JmpTo_SoundDriverLoad:                  ; ...
    2.         nop
    3.         jsr    S2_SoundDriverLoad      ; Jump to Sonic 2's SoundDriverLoad
    4.         move.w    #$100,($A11100).l
    5.         move.w    #$100,($A11200).l
    You are executing 68000 code outside of the S2 UPMEM chip.
    I will try that on hardware soon.
     
    Last edited: Jan 21, 2024
  9. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    809
    203
    43
    Lyon, France
    Sonic 2 Long Version
    Sorry for double-posting and self-quoting, but I found why I stopped investigating that route. It's because of SRAM support.

    The above works on a S2 cartridge because it's only 1Mb with no SRAM support. It will probably work on a 2Mb cartridge, again, with no SRAM support (ignoring the address bit 21). However, on a 4Mb cartridge with SRAM support (may depend of the circuitry) trying to jump and read code between $200000 and $300000 with $A130F1 set to 1 may try to read code in SRAM... and crash the system.

    My method works in all cases because $A130F1 is 0 when I'm jumping. SRAM support also works because I'm jumping back into S&K functions to handle reads and writes in SRAM. Trying to set $A130F1 to 1 to enable SRAM while executing code above $200000 crashes the system (at least with MDPro).
     
    • Informative Informative x 1
    • List
  10. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,261
    1,432
    93
    your mom
    I thought SRAM typically only covered $200000-$20FFFF? Or does it mirror/disable the rest/depend on the hardware?
     
    Last edited: Jan 21, 2024
  11. MarkeyJester

    MarkeyJester

    Original, No substitute Resident Jester
    2,203
    432
    63
    Japan
    The region $200000 to $3FFFFF is generally switched to RAM mode when bit 1 of $A130F1 is asserted:

    [​IMG]

    However, whether $200000 to $3FFFFF accesses the SRAM portion of the cartridge and how exactly, is dependent on the cartridge. It's probably why emulating this area is quite difficult and you get different results from different emulators.
     
    • Informative Informative x 5
    • List
  12. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    809
    203
    43
    Lyon, France
    Sonic 2 Long Version
    Thank you for the info MarkeyJester. That's what I guessed, it depends of the circuitry of the cartridge.
    So, I went the safer route, which is to find a way to jump above $200000 when $AF30F1 is 0.
     
  13. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,261
    1,432
    93
    your mom
    Well, that makes it tricky. Damn, that stinks lol
     
  14. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    809
    203
    43
    Lyon, France
    Sonic 2 Long Version
    I can write a full guide with my method, it works fine. Anyone interested?
     
  15. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,261
    1,432
    93
    your mom
    Looks like someone else did the same thing.

    Though, if you want a more optimal set of data (839 ($347) bytes vs. 73151 ($11DBF) bytes), here's this:
    Code (Text):
    1.         dc.w    %0101010101010101
    2.         dc.b    $00                     ; Write "00"
    3.         dc.b    $FF, $F8, $F3           ; Copy "00" 244 times
    4.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    5.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    6.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    7.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    8.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    9.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    10.  
    11.         rept    30
    12.         dc.w    %0101010101010101
    13.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    14.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    15.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    16.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    17.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    18.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    19.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    20.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    21.         endr                            ; (Replace with "endm" if using AS)
    22.  
    23.         dc.w    %0101010111110101
    24.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    25.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    26.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    27.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    28.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    29.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    30.         dc.b    $FF, $F8, $FF           ; Copy "00" 256 times
    31.         dc.b    $00, $00                ; Write "00 00"
    32.  
    33.         dc.w    %0010111100000000
    34.         dc.b    $00                     ; Write "00"
    35.         dc.l    CPU_EntryPoint_SNK      ; Write return address
    36.         dc.b    $00, $F0, $00           ; End of data
    37.         even
     
    Last edited: Jan 22, 2024
  16. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    809
    203
    43
    Lyon, France
    Sonic 2 Long Version
    Haha excellent! At least I'm not alone :)
    upload_2024-1-21_23-0-17.png
    upload_2024-1-21_22-52-30.png
     
    Last edited: Jan 21, 2024
  17. Jeffery Mewtamer

    Jeffery Mewtamer

    Blind Bookworm Member
    1,891
    101
    28
    The technical details here are way over my head, but its still fasinating to think of all the ways one could exploit this for real hardware compatible hacks that change drastically depending on whether the flash cart is plugged directly into the Genesis or the S&K cart... and Probably crazy talk, but I can't help wondering if there's a way to boot strap from a Genesis ROM into Sega CD or 32X modes... Imagine a cart that, when locked on to S&K while Sonic CD is in the Sega CD unlockes Knuckles in Sonic CD or a Chaotix Hack that when locked on adds Sonic and Tails as playable characters in Chaotix... Again, probably crazy talk, but then again, I would have thought hacking the lock-on to transform Sonic Hack into Knuckles in Sonic Hack would be crazy talk before opening this thread.

    Also, never made the connection that S&K is basically DLC before DLC was really a thing. Does that make S&K the oldest bit of DLC that hasn't become lost media? At least for a console game(no idea when expansion packs became a common thing for PC games)?
     
  18. saxman

    saxman

    Oldbie Tech Member
    Not the earliest. Wolfenstein 3D: 1992. And that might not be the earliest. But definitely beats Sonic by a few years.
     
  19. Blue Spikeball

    Blue Spikeball

    Member
    2,362
    960
    93
    Ultima 7 has it beaten by a few weeks, its expansion released earlier in the same month as Wolfenstein's. Although Jeffery Mewtamer said console games.
     
  20. Overlord

    Overlord

    Now playable in Smash Bros Ultimate Moderator
    19,247
    975
    93
    Long-term happiness
    Console games have a few rare examples of it before actual DLC becomes a thing. Off the top of my head the only other one I can think of is the GTA London expansion for GTA1, and that's on PlayStation, so years later.