Actions

SCHG How-to

Port Knuckles into Sonic 2

From Sonic Retro

(Original guide by MainMemory)
Note: this guide is written with the Sonic 2 GitHub disassembly in mind.

Getting the Knuckles object and putting it in Sonic 2

First you will need s2k.asm from the Knuckles in Sonic 2 disassembly.
Open s2k.asm, select everything from Obj01: (line 27324) to the end of ArtConvTable (line 30585), and copy it into a new asm file called Knuckles.asm in your S2 disasm folder. This is so we don't accidentally change something in our Sonic 2 disassembly while we're fixing Knuckles.
Open Knuckles.asm and do a Find and Replace operation to replace every instance of "Obj01" (without quotes) with "ObjXX", where XX is the object ID you want Knuckles to use. The following IDs are unused in Sonic 2: 4C, 4D, 4E, 4F, 62, D0, D1, and everything from DD to FF. Use that ID whenever I refer to ObjXX.
Next, do the following replacements: "CheckGameOver" to "ObjXX_CheckGameOver", "JmpTo_KillCharacter" to "JmpToK_KillCharacter", "Player_CheckFloor" to "Sonic_CheckFloor", "CheckCeilingDist" to "Sonic_CheckCeiling".

Find the line
		bsr.w	Adjust2PArtPointer2_Useless
and change it to
		jsr	Adjust2PArtPointer

Now you'll need to copy some more code from s2k.asm. Specifically, sub_3192E6, loc_318FE8, sub_318FF6, loc_319208 and loc_3193D2. Because they're all small routines, I'll paste them here:

; =============== S U B	R O U T	I N E =======================================

; Doesn't exist in S2

sub_3192E6:					  ; ...
		move.b	$17(a0),d0
		ext.w	d0
		sub.w	d0,d2
		eor.w	#$F,d2
		lea	($FFFFF768).w,a4
		move.w	#-$10,a3
		move.w	#$800,d6
		bsr.w	FindFloor
		move.b	#$80,d2

loc_319306:
		bra.w	loc_318FE8
; End of function sub_3192E6

; START	OF FUNCTION CHUNK FOR CheckRightWallDist

loc_318FE8:					  ; ...
		move.b	($FFFFF768).w,d3
		btst	#0,d3
		beq.s	return_318FF4
		move.b	d2,d3

return_318FF4:					  ; ...
		rts
; END OF FUNCTION CHUNK	FOR CheckRightWallDist

; =============== S U B	R O U T	I N E =======================================


sub_318FF6:					  ; ...
		move.b	$17(a0),d0
		ext.w	d0
		add.w	d0,d2
		lea	($FFFFF768).w,a4
		move.w	#$10,a3
		move.w	#0,d6
		bsr.w	FindFloor
		move.b	#0,d2
		bra.s	loc_318FE8
; End of function sub_318FF6

; ---------------------------------------------------------------------------
; This doesn't exist in S2...
; START	OF FUNCTION CHUNK FOR sub_315C22

loc_319208:					  ; ...
		move.b	$17(a0),d0
		ext.w	d0
		add.w	d0,d3
		lea	($FFFFF768).w,a4
		move.w	#$10,a3
		move.w	#0,d6
		bsr.w	FindWall
		move.b	#$C0,d2
		bra.w	loc_318FE8
; END OF FUNCTION CHUNK	FOR sub_315C22

; ---------------------------------------------------------------------------
; START	OF FUNCTION CHUNK FOR sub_315C22

loc_3193D2:					  ; ...
		move.b	$17(a0),d0
		ext.w	d0
		sub.w	d0,d3
		eor.w	#$F,d3
		lea	($FFFFF768).w,a4
		move.w	#$FFF0,a3
		move.w	#$400,d6
		bsr.w	FindWall
		move.b	#$40,d2
		bra.w	loc_318FE8
; END OF FUNCTION CHUNK	FOR sub_315C22

Put this code at the end of Knuckles' asm.

Now we're ready to put the code in our S2 disassembly, so open s2.asm. You can place Knuckles' code wherever you want, but the place that seems to cause the fewest errors is right before Obj79. You can either add the line
include "Knuckles.asm"
or just copy and paste his code in.

When you do that, run build.bat in the S2 disassembly. It should tell you there are 14 errors and to check s2.log, so that's what we'll do. Everything in the log will be in this format:

> > >s2.asm(line#): error: jump distance too big
> > > 	bsr.w	label

What you need to do is go to each line and change the bsr.w to jsr. There is one line that's a bra.w instead, change that to jmp.

Importing Knuckles' art and mappings

If you try to build now, you will see three "symbol undefined" errors: SK_Map_Knuckles, SK_PLC_Knuckles and SK_ArtUnc_Knux. So let's fix those.

For SK_Map_Knuckles, you need to take Knuckles' sprite mappings from S&K and convert them to Sonic 2 format. There are two ways to do this:

  • Using SonMapEd, set the Game Format setting to "Sonic 3 & Knuckles", load the mappings file, change the Game Format setting to "Sonic 2", and save the mappings as Knuckles.asm in the mappings/sprite folder of your S2 disasm.
  • Using MappingsConverter, load the mappings with input set to Game: "Sonic 3 & Knuckles", Format: ASM and output set to Game: "Sonic 2", Format: ASM, and save the mappings as Knuckles.asm in the mappings/sprite folder of your S2 disasm.
    • If you're using the MapMacros branch of the S2 disassembly as a base, you can also set the output format to "ASM with Macros".
Then, in s2.asm, somewhere near Mapunc_Sonic, add the line
SK_Map_Knuckles:	include	"mappings/sprite/Knuckles.asm"


For SK_PLC_Knuckles, you can simply take Knuckles' DPLCs from S&K and save them as Knuckles.asm in the mappings/spriteDPLC folder of your S2 disasm, since the DPLC format is the same for S3K's players and Sonic 2.

Then, in s2.asm, somewhere near MapRUnc_Sonic, add the line
SK_PLC_Knuckles:	include	"mappings/spriteDPLC/Knuckles.asm"


For SK_ArtUnc_Knux, take Knuckles' art from S&K and save it as Knuckles.bin in the art/uncompressed folder of your S2 disasm.

Then, in s2.asm, somewhere near ArtUnc_Sonic, add the lines
	align $20
SK_ArtUnc_Knux:	BINCLUDE	"art/uncompressed/Knuckles.bin"

Optimizing Knuckles' art loading

This step is optional, unless you use the Sonic 2 Clone Driver (v2), but is recommended for performance's sake.
You may have noticed that Knuckles' art file is the same file as S3K, even though the two games' palettes are organized differently. This is possible because Knuckles' code dynamically recolors all his sprites in LoadKnucklesDynPLC_Part2. This saves some space in the KiS2 ROM, but for a Sonic 2 hack, it's just wasting CPU, so we'll remove it.
Select everything under the line bmi.w return_31753E in LoadKnucklesDynPLC_Part2 up to (but not including) return_31753E, and delete it. Replace it with this code, modified from LoadSonicDynPLC_Part2:

	move.w	#tiles_to_bytes(ArtTile_ArtUnc_Sonic),d4
; loc_1B86E:
KPLC_ReadEntry:
	moveq	#0,d1
	move.w	(a2)+,d1
	move.w	d1,d3
	lsr.w	#8,d3
	andi.w	#$F0,d3
	addi.w	#$10,d3
	andi.w	#$FFF,d1
	lsl.l	#5,d1
	addi.l	#SK_ArtUnc_Knux,d1
	move.w	d4,d2
	add.w	d3,d4
	add.w	d3,d4
	jsr	(QueueDMATransfer).l
	dbf	d5,KPLC_ReadEntry	; repeat for number of entries

Then you can delete ArtConvTable and the comments above it.
Now you just need Knuckles' art converted to Sonic 2's palette, and replace Knuckles' art file (art/uncompressed/Knuckles.bin) with that.

Getting Knuckles ingame

At this point, you should be getting no errors when building, but you still can't play as Knuckles, because nothing references his code!
Go to Obj_Index in s2.asm, find the line corresponding to the object ID you gave Knuckles' code, and replace it with this:

ObjPtr_Knuckles:	dc.l ObjXX	; Knuckles
Now, go to s2.constants.asm, find the end of the ObjID list, and add
ObjID_Knuckles =		id(ObjPtr_Knuckles)

Back in s2.asm, under InitPlayers, copy the first two lines under InitPlayers_Alone and paste them under InitPlayers_TailsAlone, changing the label after the bne to InitPlayers_KnucklesAlone. Then add the following lines under the rts at the end of InitPlayers_TailsAlone:

InitPlayers_KnucklesAlone:
	move.b	#ObjID_Knuckles,(MainCharacter+id).w ; load ObjXX Knuckles object at $FFFFB000
	move.b	#ObjID_SpindashDust,(Sonic_Dust+id).w ; load Obj08 Knuckles' spindash dust/splash object at $FFFFD100
	rts

Next go to OptionScreen_Choices and change the 3 to a 4 in the first line. This changes how many characters are available in the character select.

Under off_92D2 and off_92DE add the following line at the end of the list:
	dc.l TextOptScr_KnucklesAlone
Then go to TextOptScr_TailsAlone and add the line
TextOptScr_KnucklesAlone:		menutxt	"KNUCKLES ALONE "

Now build the game, run it, and choose Knuckles in the options menu, and you'll see... Blue Knuckles!

Importing Knuckles' palettes

Here are the palette files, put them in the art/palettes folder.
Add the following lines under Pal_Result for the palette data:

Pal_Knux:  palette Knuckles.bin,SonicAndTails2.bin ; "Sonic and Miles" background palette (also usually the primary palette line)
Pal_CPZ_K_U: palette CPZ Knux underwater.bin ; Chemical Plant Zone underwater palette
Pal_ARZ_K_U: palette ARZ Knux underwater.bin ; Aquatic Ruin Zone underwater palette

Add these lines under PalPtr_Result to add them to the palette list:

PalPtr_Knux:	palptr Pal_Knux,  0
PalPtr_CPZ_K_U:	palptr Pal_CPZ_K_U, 0
PalPtr_ARZ_K_U:	palptr Pal_ARZ_K_U, 0

In s2.constants.asm, find the end of the PalID list, and add the lines

PalID_Knux =	id(PalPtr_Knux) ; 28
PalID_CPZ_K_U =	id(PalPtr_CPZ_K_U) ; 29
PalID_ARZ_K_U =	id(PalPtr_ARZ_K_U) ; 30

Now find Level_LoadPal in s2.asm, and replace everything until Level_WaterPal with

Level_LoadPal:
	moveq	#PalID_BGND,d0
	cmpi.w	#3,(Player_mode).w	; are you playing as Knuckles?
	blt.s	+	; if not, branch
	moveq	#PalID_Knux,d0	; load Knuckles' palette index
+	bsr.w	PalLoad_Now	; load Sonic's palette line
	tst.b	(Water_flag).w	; does level have water?
	beq.s	Level_GetBgm	; if not, branch
	moveq	#PalID_HPZ_U,d0	; palette number $15
	cmpi.b	#hidden_palace_zone,(Current_Zone).w
	beq.s	Level_WaterPal ; branch if level is HPZ
	moveq	#PalID_CPZ_U,d0	; palette number $16
	cmpi.b	#chemical_plant_zone,(Current_Zone).w
	bne.s	Level_PalNotCPZ ; branch if level is not CPZ
	cmpi.w	#3,(Player_mode).w	; are you playing as Knuckles?
	blt.s	Level_WaterPal	; if not, branch
	moveq	#PalID_CPZ_K_U,d0
	bra.s	Level_WaterPal	; branch
Level_PalNotCPZ:
	moveq	#PalID_ARZ_U,d0	; palette number $17
	cmpi.w	#3,(Player_mode).w	; are you playing as Knuckles?
	blt.s	Level_WaterPal	; if not, branch
	moveq	#PalID_ARZ_K_U,d0

Super Knuckles

In s2k.asm, select everything from PalCycle_SuperKnuckles (line 3173) to Pal_KnucklesReds (line 3227) and paste it into s2.asm just before Pal_FadeTo. Then, under PalCycle_SuperSonic, add the lines

	cmpi.w	#3,(Player_mode).w	; are we Knuckles?
	beq.w	PalCycle_SuperKnuckles	; if we are, run the Super Knuckles cycle instead

Fixing bugs

Monitors don't break when gliding

In SolidObject_Monitor_Sonic, just under bne.s Obj26_ChkOverEdge, add the following lines:

	cmpi.b	#ObjID_Knuckles,id(a1)
	beq.s	SolidObject_Monitor_Knuckles

Then, under the rts, add

SolidObject_Monitor_Knuckles:
	cmp.b	#1,$21(a1)
	beq.s	+
	cmp.b	#3,$21(a1)
	beq.s	+
	cmpi.b	#AniIDSonAni_Roll,anim(a1)
	bne.w	SolidObject_cont
+	rts

Then, under loc_3F768, replace everything from cmpi.b #2,anim(a0) up to return_3F78A with

	cmpi.b	#2,anim(a0)
	beq.s	Break_Monitor
	cmpi.b	#ObjID_Knuckles,id(a0)
	bne.s	return_3F78A
	cmp.b	#1,$21(a0)
	beq.s	Break_Monitor
	cmp.b	#3,$21(a0)
	bne.s	return_3F78A

Break_Monitor:
	neg.w	y_vel(a0)	; reverse Sonic's y-motion
	move.b	#4,routine(a1)
	move.w	a0,parent(a1)

Knuckles gets hurt gliding into enemies

Under Touch_Enemy, add the following lines:

	cmpi.b	#ObjID_Knuckles,id(a0)
	bne.s	Touch_NotKnuckles
	cmp.b	#1,$21(a0)
	beq.s	+
	cmp.b	#3,$21(a0)
	beq.s	+

Touch_NotKnuckles:

Knuckles doesn't behave properly when standing on objects

Under Sonic_ResetOnFloor_Part2, add the following lines:

	cmpi.b	#ObjID_Knuckles,id(a0)	; is this object ID Knuckles?
	beq.w	Knuckles_ResetOnFloor_Part2	; if it is, branch to the Knuckles version of this code

Knuckles doesn't bounce off bosses when gliding

Under Touch_Enemy, just before neg.w x_vel(a0) add the following lines:

	cmpi.b	#ObjID_Knuckles,id(a0)
	bne.s	+
	cmp.b	#1,$21(a0)
	bne.s	+
	move.b	#2,$21(a0)
	move.b	#$21,anim(a0)
+

Then add the same lines just before the neg.w x_vel(a0) under Touch_Enemy_Part2.

Dust doesn't appear when Knuckles is sliding

Change Obj08_CheckSkid to this:

Obj08_CheckSkid:
	movea.w	parent(a0),a2 ; a2=character
	moveq	#$10,d1	; move y offset to d1
	cmpi.b	#AniIDSonAni_Stop,anim(a2)	; SonAni_Stop
	beq.s	Obj08_SkidDust
	moveq	#6,d1	; move different y offset to d1
	cmpi.b	#ObjID_Knuckles,id(a2)	; playing as Knuckles?
	bne.s	+
	cmpi.b	#3,$21(a2)	; check for sliding
	beq.s	Obj08_SkidDust
+	move.b	#2,routine(a0)
	move.b	#0,objoff_32(a0)
	rts
Then, under Obj08_SkidDust, change the line
addi.w	#$10,y_pos(a1)
to
add.w	d1,y_pos(a1)

Knuckles' height is incorrect when breathing a bubble

Under loc_1FB0C, find the lines

	cmpi.b	#1,(a1)
	bne.s	loc_1FBA8

and replace them with

	cmpi.b	#ObjID_Knuckles,id(a1)
	beq.s	+
	cmpi.b	#1,(a1)
	bne.s	loc_1FBA8
+

Knuckles' sprite is incorrect when exiting debug mode

In loc_41C56, add the following lines under move.l #Mapunc_Sonic,mappings(a1):

	cmpi.b	#ObjID_Knuckles,id(a1)
	bne.s	+
	move.l	#SK_Map_Knuckles,mappings(a1)
+

Knuckles starts gliding during the WFZ end scene

Under ObjB2_Jump_to_plane, find the line
	move.w	#((button_right_mask|button_A_mask)<<8)|button_right_mask|button_A_mask,(Ctrl_1_Logical).w
and replace it with
	move.w	#(button_right_mask|button_A_mask)<<8,(Ctrl_1_Logical).w

Then, just above loc_3AB8A, add

	addq.b	#2,routine_secondary(a0)
	bra.s	loc_3AB8A

ObjB2_Jumping_to_ship:
	cmpi.w	#$447,objoff_2A(a0)
	bcc.s	loc_3AB8A
	move.w	#button_A_mask<<8,(Ctrl_1_Logical).w
Under ObjB2_Main_WFZ_states, just before the last item, add
		offsetTableEntry.w ObjB2_Jumping_to_ship

Knuckles doesn't stop gliding on OOZ fans

Under loc_2A990 go to the + label and insert the following lines:

	cmpi.b	#ObjID_Knuckles,id(a1)
	bne.s	+
	clr.b	$21(a1)
+

Knuckles doesn't stop gliding on WFZ propellers

Under ObjB5_CheckPlayer go to the + label and insert the following lines:

	cmpi.b	#ObjID_Knuckles,id(a1)
	bne.s	ObjB5_NotKnuckles
	clr.b	$21(a1)

ObjB5_NotKnuckles:

Knuckles doesn't stop gliding in WFZ wind tunnels

Under WindTunnel find the line bset #1,status(a1) and insert the following lines:

	cmpi.b	#ObjID_Knuckles,id(a1)
	bne.s	+
	clr.b	$21(a1)

+

The level doesn't reset properly after dying

Under ObjXX_ResetLevel_Part2, change the line
	tst.w	($FFFFFFDC).w
to
	tst.w	(Two_player_mode).w

Importing graphics from KiS2

Importing and PLC setup

In s2k.asm, copy everything from ArtNem_MiniKnuckles (line 75253) up to ArtNem_TitleSprites_1 (line 76054), and paste it anywhere you want in s2.asm, like after ArtNem_MiniTails.
Under PlrList_ResultsTails_End, add the lines

;---------------------------------------------------------------------------------------
; Pattern load queue
; Knuckles life counter
;---------------------------------------------------------------------------------------
PlrList_KnucklesLife: plrlistheader
	plreq ArtTile_ArtNem_life_counter, ArtNem_KTELife
PlrList_KnucklesLife_End
;---------------------------------------------------------------------------------------
; PATTERN LOAD REQUEST LIST
; Standard 2 - loaded for every level
;---------------------------------------------------------------------------------------
PlrList_Std2Knuckles: plrlistheader
	plreq ArtTile_ArtNem_Checkpoint, ArtNem_Checkpoint
	plreq ArtTile_ArtNem_Powerups, ArtNem_Powerups
	plreq ArtTile_ArtNem_Powerups+$2C, ArtNem_MonitorIconsMod
	plreq ArtTile_ArtNem_Shield, ArtNem_InvincibilityShield
PlrList_Std2Knuckles_End
;---------------------------------------------------------------------------------------
; Pattern load queue
; Knuckles end of level results screen
;---------------------------------------------------------------------------------------
PlrList_ResultsKnuckles: plrlistheader
	plreq ArtTile_ArtNem_TitleCard, ArtNem_TitleCard
	plreq ArtTile_ArtNem_ResultsText, ArtNem_ResultsText
	plreq ArtTile_ArtNem_MiniCharacter, ArtNem_MiniKnuckles
	plreq ArtTile_ArtNem_Perfect, ArtNem_Perfect
PlrList_ResultsKnuckles_End
;---------------------------------------------------------------------------------------
; Pattern load queue
; End of level signpost
;---------------------------------------------------------------------------------------
PlrList_SignpostKnuckles: plrlistheader
	plreq ArtTile_ArtNem_Signpost, ArtNem_Signpost
	plreq ArtTile_ArtNem_Signpost+$22, ArtNem_Signpost_KnucklesPatch
PlrList_SignpostKnuckles_End

Then under PLCptr_ResultsTails, add

PLCptr_KnucklesLife:	offsetTableEntry.w PlrList_KnucklesLife
PLCptr_Std2Knuckles:	offsetTableEntry.w PlrList_Std2Knuckles
PLCptr_ResultsKnuckles:	offsetTableEntry.w PlrList_ResultsKnuckles
PLCptr_SignpostKnuckles:	offsetTableEntry.w PlrList_SignpostKnuckles

In s2.constants.asm, under PLCID_ResultsTails add the lines

PLCID_KnucklesLife =	id(PLCptr_KnucklesLife)
PLCID_Std2Knuckles =	id(PLCptr_Std2Knuckles)
PLCID_ResultsKnuckles =	id(PLCptr_ResultsKnuckles)
PLCID_SignpostKnuckles =	id(PLCptr_SignpostKnuckles)

Loading the PLCs

Now that the art is in the PLC list, we need to tell the game to load it.
Under Level, find the line moveq #PLCID_Std2,d0 and replace everything from there up to Level_ClrRam with

	bsr.w	Level_SetPlayerMode
	moveq	#PLCID_Std2,d0
	cmpi.w	#3,(Player_mode).w	; are we Knuckles?
	bne.s	+	; if not, branch
	moveq	#PLCID_Std2Knuckles,d0	; load Knuckles' standard art
+	bsr.w	LoadPLC
	moveq	#PLCID_Miles1up,d0
	tst.w	(Two_player_mode).w
	bne.s	+
	cmpi.w	#2,(Player_mode).w
	bne.s	Level_NoTails
	addq.w	#PLCID_MilesLife-PLCID_Miles1up,d0
+
	tst.b	(Graphics_Flags).w
	bpl.s	+
	addq.w	#PLCID_Tails1up-PLCID_Miles1up,d0
+
	bsr.w	LoadPLC

Level_NoTails:
	cmpi.w	#3,(Player_mode).w
	bne.s	Level_ClrRam
	moveq	#PLCID_KnucklesLife,d0
	bsr.w	LoadPLC

In CheckLoadSignpostArt find the line moveq #PLCID_Signpost,d0 and under it, add

	cmpi.w	#3,(Player_mode).w
	bne.w	LoadPLC2		; load signpost art
	moveq	#PLCID_SignpostKnuckles,d0

Additionally change the bne.s + under tst.w (Two_player_mode).w to bne.s ++
Then under Load_EndOfAct, insert the following just before jsr (LoadPLC2).l:

	cmpi.w	#3,(Player_mode).w
	bne.s	+
	moveq	#PLCID_ResultsKnuckles,d0
+

Life Icon mappings

In KiS2, Knuckles' initials in the life counter use palette line 1, but S2 uses line 2. To fix this, take this file and extract it to the mappings/sprite folder.

Then, under HUD_MapUnc_40C82 add
HUD_MapUnc_Knuckles:	BINCLUDE "mappings/sprite/hud_k.bin"

Next go to BuildHUD, find the line lea (HUD_MapUnc_40A9A).l,a1 and add this below it:

	cmpi.w	#3,(Player_mode).w
	bne.s	+
	lea	(HUD_MapUnc_Knuckles).l,a1
+

Score Tally

To get the score tally to display Knuckles' name, add the following lines just before Obj6F_MapUnc_14ED0:

Map_Obj3A_Knuckles:	offsetTable
	offsetTableEntry.w word_311BF6
	offsetTableEntry.w word_14D1C
	offsetTableEntry.w word_14D5E
	offsetTableEntry.w word_14DA0
	offsetTableEntry.w word_14DDA
	offsetTableEntry.w word_14BC8
	offsetTableEntry.w word_14BEA
	offsetTableEntry.w word_14BF4
	offsetTableEntry.w word_14BFE
	offsetTableEntry.w word_14DF4
	offsetTableEntry.w word_14E1E
	offsetTableEntry.w word_14E50
	offsetTableEntry.w word_14E82
	offsetTableEntry.w word_14E8C
	offsetTableEntry.w word_14E96
word_311BF6:	dc.w $B
	dc.w 5, $85C6, $82E3, $FF88
	dc.w 5, $8584, $82C2, $FF98
	dc.w 5, $85D8, $82EC, $FFA8
	dc.w 5, $85B4, $82DA, $FFB8
	dc.w 5, $85C6, $82E3, $FFC8
	dc.w 5, $85C2, $82E1, $FFD8
	dc.w 5, $8580, $82C0, $FFE8
	dc.w 5, $85D0, $82E8, $FFF8
	dc.w 5, $85B8, $82DC, $10
	dc.w 5, $8588, $82C4, $20
	dc.w 5, $85D4, $82EA, $2F

Then, in loc_140CE find the line move.l #Obj3A_MapUnc_14CBC,mappings(a1) and add the following under it:

	cmpi.w	#3,(Player_mode).w
	bne.s	+
	move.l	#Map_Obj3A_Knuckles,mappings(a1)
+

Under the last results_screen_object in byte_14380, add

byte_14380_K:
	results_screen_object   $28, $138,  $B8,   2,  0
	results_screen_object  $200, $100,  $CA,   4,  3
	results_screen_object  $240, $140,  $CA,   6,  4
	results_screen_object  $278, $178,  $BE,   8,  6
	results_screen_object  $350, $120, $120,   4,  9
	results_screen_object  $320, $120,  $F0,   4, $A
	results_screen_object  $330, $120, $100,   4, $B
	results_screen_object  $340, $120, $110, $16, $E

Then, in loc_140AC find lea byte_14380(pc),a2 and under it add

	cmpi.w	#3,(Player_mode).w
	bne.s	+
	lea	byte_14380_K(pc),a2
+

Download this file and put it in the art/nemesis folder. Then, under ArtNem_TitleCard2 add the lines

	even
ArtNem_KnucklesK:	BINCLUDE	"art/nemesis/S2KnuxK.bin"
Finally under PlrList_ResultsKnuckles (remember that?) add the line
	plreq ArtTile_ArtNem_ResultsText+$16, ArtNem_KnucklesK

at the end of the list.

Title Card background

To make Knuckles' title card background green instead of red, go to LoadTitleCard0, and under bsr.w JmpTo2_NemDec add

	cmpi.w	#3,(Player_option).w
	bne.s	LoadTitleCard_Art2
	move.l	#vdpComm(tiles_to_bytes(ArtTile_ArtNem_TitleCard+$5A),VRAM,WRITE),(VDP_control_port).l
	moveq	#$F,d0

loc_312364:
	move.l	#$44444444,(VDP_data_port).l
	dbf	d0,loc_312364
LoadTitleCard_Art2:

CNZ slot machines

To get Casino Night Zone's slot machines to show Knuckles instead of Sonic, go to SlotMachine_GetPixelRow and add

	cmpi.w	#3,(Player_mode).w
	beq.s	sub_325964

Then, under the rts, paste this routine from KiS2:

sub_325964:					  ; ...
		move.w	d3,d0
		lsr.w	#8,d0
		and.w	#7,d0
		move.b	(a3,d0.w),d0
		and.w	#7,d0
		beq.s	loc_32598C
		ror.w	#7,d0
		lea	(ArtUnc_CNZSlotPics).l,a2
		add.w	d0,a2
		move.w	d3,d0
		and.w	#$F8,d0
		lsr.w	#1,d0
		add.w	d0,a2
		rts
; ---------------------------------------------------------------------------

loc_32598C:					  ; ...
		lea	(byte_33B1F0).l,a2
		move.w	d3,d0
		and.w	#$F8,d0
		lsr.w	#1,d0
		add.w	d0,a2
		rts
; End of function sub_325964

Ending Sequence

Note: This section is not finished.

Under EndingSequence find the line move.w d0,(Ending_Routine).w and just above it add

	cmpi.w	#3,(Player_mode).w
	bne.s	+
	moveq	#6,d0
	cmpi.b	#7,(Emerald_count).w
	bne.s	+
	addq.w	#2,d0
+

Then under EndingSequence_LoadCharacterArt_Characters add the lines

	offsetTableEntry.w EndingSequence_LoadCharacterArt_Knuckles	; 6
	offsetTableEntry.w EndingSequence_LoadCharacterArt_Knuckles	; 8

at the end of the table.
Under the bra.w at the end of EndingSequence_LoadCharacterArt_Tails, add the lines

EndingSequence_LoadCharacterArt_Knuckles:
	move.l	#vdpComm(tiles_to_bytes(ArtTile_EndingCharacter),VRAM,WRITE),(VDP_control_port).l
	lea	(ArtNem_EndingKnuckles).l,a0
	bra.w	JmpTo_NemDec

Under EndingSequence_LoadFlickyArt_Flickies add the lines

	offsetTableEntry.w EndingSequence_LoadFlickyArt_Bird	; 6
	offsetTableEntry.w EndingSequence_LoadFlickyArt_Eagle	; 8

at the end of the table.
Under ObjCA, change the bit above st (Super_Sonic_flag).w to

	addq.w	#1,objoff_32(a0)
	cmpi.w	#8,(Ending_Routine).w
	beq.s	+
	cmpi.w	#2,(Ending_Routine).w
	beq.s	+
	bra.s	++
+

Under ObjCA_State5_States add the lines

	offsetTableEntry.w loc_A2E0k	; 6
	offsetTableEntry.w loc_A2EEk	; 8

at the end of the table.
Under the rts at the end of loc_A2F2, add the lines

loc_A2E0k:
	moveq	#$10,d0
-
	move.b	#ObjID_Knuckles,id(a1) ; load Sonic object
	move.b	#$81,obj_control(a1)
	rts
; ===========================================================================

loc_A2EEk:
	moveq	#$12,d0
	bra.s	-

Under PaletteChangerDataIndex add the lines

	offsetTableEntry.w off_310678	;$10
	offsetTableEntry.w off_310690	;$12

at the end of the table.
Under off_133E0, add the lines

off_310678:	C9PalInfo    loc_1344C,Pal_3090BC,   0,$1F,4,7
off_310690:	C9PalInfo    loc_1344C,Pal_3090BC+$C0,0,$F,4,7

Then in s2k.asm, take everything from Pal_3090BC (line 11400) up to dword_30919C (line 11406) and paste it in s2.asm under Pal_AD3E.
Under loc_A30A find the line cmpi.w #2,(Ending_Routine).w and replace everything from there to move.b #AniIDSonAni_Walk,anim(a1) with:

	cmpi.w	#8,(Ending_Routine).w
	beq.s	+
	cmpi.w	#2,(Ending_Routine).w
	beq.s	+
	bra.s	++
+

Under loc_A4B6, find the line move.b #7,mapping_frame(a0) and just above it, add

	cmpi.w	#8,(Ending_Routine).w
	beq.s	+

Under off_A534, add the lines

		offsetTableEntry.w loc_A53Ak	; 6
		offsetTableEntry.w loc_A55C	; 8

at the end of the table.
Under the bra.s - at the end of loc_A582, add:

loc_A53Ak:
	move.w	y_pos(a0),d0
	subi.w	#$1C,d0
	move.w	d0,y_pos(a1)
	move.w	x_pos(a0),x_pos(a1)
	move.w	#$500,anim(a1)
	move.w	#$100,anim_frame_duration(a1)
	rts

Under off_A5FC, add the lines

		offsetTableEntry.w byte_A602	; 6
		offsetTableEntry.w byte_A61E	; 8

at the end of the table.

Adding Knuckles' sound effects from S3K

You may notice that Knuckles is missing some sound effects for when he grabs a wall, lands from a glide, and slides along the ground. Since KiS2 uses S2's sound driver directly, they couldn't add these sounds, but you can.
Download this file and extract its contents to the sound folder of your disassembly.
Just above MusicPoint2 add the lines:

SonicDriverVer = 2
	include	"sound/_smps2asm_inc.asm"

Under SoundIndex, add the following lines anywhere before SndPtr__End:

SndPtr_WallGrab:	rom_ptr_z80	Snd_WallGrab
SndPtr_Land:		rom_ptr_z80	Snd_Land
SndPtr_Slide:		rom_ptr_z80	Snd_Slide

Their position within the list determones their priority: anything below them will be interrupted by these sounds, and anything above them can interrupt them. I personally put them just above SndPtr_Ring.
Then after the end of Sound70, above the if line, add

Snd_WallGrab:	include	"sound/wallgrab.asm"
Snd_Land:	include	"sound/land.asm"
Snd_Slide:	include	"sound/slide.asm"

If you try to build now, you will likely get an error telling you the sound bank is too large. In that case, you can go above SoundIndex and add

	align $8000
soundBankStart	:= *

This will put the sound effects in their own sound bank.
In s2.constants.asm anywhere between SndID__First and SndID__End (it doesn't matter where) add

SndID_WallGrab =	id(SndPtr_WallGrab)
SndID_Land =		id(SndPtr_Land)
SndID_Slide =		id(SndPtr_Slide)

Now you're ready to get Knuckles to play the sounds.
Go to loc_315804 and replace the rts with:

		move.w	#SndID_WallGrab,d0
		jmp	PlaySound

Just above return_315900 add:

		move.w	#SndID_Land,d0
		jmp	PlaySound

Then under loc_315958, just above the rts add:

		move.b	(Timer_frames+1).w,d0
		andi.b	#7,d0
		bne.s	+
		moveq	#SndID_Slide,d0
		jmp	PlaySound
+

Now depending on where you placed the new sounds, you may have broken some of Knuckles' code which uses numbers rather than SndID constants.
To fix this you can search for every occurrence of PlaySound, and change the move.w line just above it to use a SndID constant.
Luckily for you, the SndID definitions in s2.constants.asm all have their original values as comments, so if you find

		move.w	#$A4,d0
		jsr	PlaySound

you can search the SndID list to find

SndID_Skidding =	id(SndPtr_Skidding)		; A4

and you'll replace the $A4 with SndID_Skidding.

Once you do all this and test, you will notice that the sliding sound is not working. This is because a slight change to the driver is needed to get it to work. So open up s2.sounddriver.asm and locate this:

; zloc_285:
;zGetFrequency
zFMSetFreq:
	; 'a' holds a note to get frequency for
	sub	80h
	jr	z,zFMDoRest		; If this is a rest, jump to zFMDoRest
	add	a,(ix+5)		; Add current channel key offset (coord flag E9)
	add	a,a				; Offset into Frequency table...
	add	a,zFrequencies&0FFh
	ld	(zloc_292+2),a	; store into the instruction after zloc_292 (self-modifying code)
;	ld	d,a
;	adc	a,(zFrequencies&0FF00h)>>8
;	sub	d
;	ld	(zloc_292+3),a	; this is how you could store the high byte of the pointer too (unnecessary if it's in the right range)
zloc_292:

There are no issues if the comments are not there; in any event, you want to edit it to match this:

; zloc_285:
;zGetFrequency
zFMSetFreq:
	; 'a' holds a note to get frequency for
	sub	80h
	jr	z,zFMDoRest		; If this is a rest, jump to zFMDoRest
	add	a,(ix+5)		; Add current channel key offset (coord flag E9)
	add	a,a				; Offset into Frequency table...
	add	a,zFrequencies&0FFh
	ld	(zloc_292+2),a	; store into the instruction after zloc_292 (self-modifying code)
	ld	d,a
	adc	a,(zFrequencies&0FF00h)>>8
	sub	d
	ld	(zloc_292+3),a	; this is how you could store the high byte of the pointer too (unnecessary if it's in the right range)
zloc_292:

After you are done, find this:

;zloc_460
zPSGSetFreq:
	sub	81h				; a = a-$81 (zero-based index from lowest note)
	jr	c,+				; If carry (only time that happens if 80h because of earlier logic) this is a rest!
	add	a,(ix+5)		; Add current channel key offset (coord flag E9)
	add	a,a				; Multiply note value by 2
	add	a,zPSGFrequencies&0FFh	; Point to proper place in table
	ld	(zloc_46D+2),a	; store into the instruction after zloc_46D (self-modifying code)
zloc_46D:

and change it to this:

;zloc_460
zPSGSetFreq:
	sub	81h				; a = a-$81 (zero-based index from lowest note)
	jr	c,+				; If carry (only time that happens if 80h because of earlier logic) this is a rest!
	add	a,(ix+5)		; Add current channel key offset (coord flag E9)
	add	a,a				; Multiply note value by 2
	add	a,zPSGFrequencies&0FFh	; Point to proper place in table
	ld	(zloc_46D+2),a	; store into the instruction after zloc_46D (self-modifying code)
	ld	d,a
	adc	a,(zPSGFrequencies&0FF00h)>>8
	sub	d
	ld	(zloc_46D+3),a	; this is how you could store the high byte of the pointer too (unnecessary if it's in the right range)
zloc_46D:

We will be moving the zFrequencies and zPSGFrequencies tables to they don't get in the way, and these changes load the high byte of the pointer too. So now time to move the tables: find this:

; This the note -> frequency setting lookup
; the same array is found at $729CE in Sonic 1, and at $C9C44 in Ristar
; zword_359:
	ensure1byteoffset 8Ch
zPSGFrequencies:
	dw	356h,  326h, 2F9h, 2CEh, 2A5h, 280h, 25Ch, 23Ah
	dw	21Ah,  1FBh, 1DFh, 1C4h, 1ABh, 193h, 17Dh, 167h
	dw	153h,  140h, 12Eh, 11Dh, 10Dh, 0FEh, 0EFh, 0E2h
	dw	0D6h,  0C9h, 0BEh, 0B4h, 0A9h, 0A0h,  97h,  8Fh
	dw	 87h,   7Fh,  78h,  71h,  6Bh,  65h,  5Fh,  5Ah
	dw	 55h,   50h,  4Bh,  47h,  43h,  40h,  3Ch,  39h
	dw	 36h,   33h,  30h,  2Dh,  2Bh,  28h,  26h,  24h
	dw	 22h,   20h,  1Fh,  1Dh,  1Bh,  1Ah,  18h,  17h
	dw	 16h,   15h,  13h,  12h,  11h,    0
; ---------------------------------------------------------------------------

and delete it; then find this:

; lookup table of FM note frequencies for instruments and sound effects
	ensure1byteoffset 0C0h
; zbyte_534:
zFrequencies:
	dw 025Eh,0284h,02ABh,02D3h,02FEh,032Dh,035Ch,038Fh,03C5h,03FFh,043Ch,047Ch
	dw 0A5Eh,0A84h,0AABh,0AD3h,0AFEh,0B2Dh,0B5Ch,0B8Fh,0BC5h,0BFFh,0C3Ch,0C7Ch
	dw 125Eh,1284h,12ABh,12D3h,12FEh,132Dh,135Ch,138Fh,13C5h,13FFh,143Ch,147Ch
	dw 1A5Eh,1A84h,1AABh,1AD3h,1AFEh,1B2Dh,1B5Ch,1B8Fh,1BC5h,1BFFh,1C3Ch,1C7Ch
	dw 225Eh,2284h,22ABh,22D3h,22FEh,232Dh,235Ch,238Fh,23C5h,23FFh,243Ch,247Ch
	dw 2A5Eh,2A84h,2AABh,2AD3h,2AFEh,2B2Dh,2B5Ch,2B8Fh,2BC5h,2BFFh,2C3Ch,2C7Ch
	dw 325Eh,3284h,32ABh,32D3h,32FEh,332Dh,335Ch,338Fh,33C5h,33FFh,343Ch,347Ch
	dw 3A5Eh,3A84h,3AABh,3AD3h,3AFEh,3B2Dh,3B5Ch,3B8Fh,3BC5h,3BFFh,3C3Ch,3C7Ch ; 96 entries

and delete it too; now find this:

; ---------------------------------------------------------------------------
;zbyte_FD8h
zSFXPriority:

and edit it to match this:

; ---------------------------------------------------------------------------
; This the note -> frequency setting lookup
; the same array is found at $729CE in Sonic 1, and at $C9C44 in Ristar
; zword_359:
zPSGFrequencies:
	dw	356h,  326h, 2F9h, 2CEh, 2A5h, 280h, 25Ch, 23Ah
	dw	21Ah,  1FBh, 1DFh, 1C4h, 1ABh, 193h, 17Dh, 167h
	dw	153h,  140h, 12Eh, 11Dh, 10Dh, 0FEh, 0EFh, 0E2h
	dw	0D6h,  0C9h, 0BEh, 0B4h, 0A9h, 0A0h,  97h,  8Fh
	dw	 87h,   7Fh,  78h,  71h,  6Bh,  65h,  5Fh,  5Ah
	dw	 55h,   50h,  4Bh,  47h,  43h,  40h,  3Ch,  39h
	dw	 36h,   33h,  30h,  2Dh,  2Bh,  28h,  26h,  24h
	dw	 22h,   20h,  1Fh,  1Dh,  1Bh,  1Ah,  18h,  17h
	dw	 16h,   15h,  13h,  12h,  11h,    0,    0,    0
; lookup table of FM note frequencies for instruments and sound effects
; zbyte_534:
zFrequencies:
	dw 025Eh,0284h,02ABh,02D3h,02FEh,032Dh,035Ch,038Fh,03C5h,03FFh,043Ch,047Ch
	dw 0A5Eh,0A84h,0AABh,0AD3h,0AFEh,0B2Dh,0B5Ch,0B8Fh,0BC5h,0BFFh,0C3Ch,0C7Ch
	dw 125Eh,1284h,12ABh,12D3h,12FEh,132Dh,135Ch,138Fh,13C5h,13FFh,143Ch,147Ch
	dw 1A5Eh,1A84h,1AABh,1AD3h,1AFEh,1B2Dh,1B5Ch,1B8Fh,1BC5h,1BFFh,1C3Ch,1C7Ch
	dw 225Eh,2284h,22ABh,22D3h,22FEh,232Dh,235Ch,238Fh,23C5h,23FFh,243Ch,247Ch
	dw 2A5Eh,2A84h,2AABh,2AD3h,2AFEh,2B2Dh,2B5Ch,2B8Fh,2BC5h,2BFFh,2C3Ch,2C7Ch
	dw 325Eh,3284h,32ABh,32D3h,32FEh,332Dh,335Ch,338Fh,33C5h,33FFh,343Ch,347Ch
	dw 3A5Eh,3A84h,3AABh,3AD3h,3AFEh,3B2Dh,3B5Ch,3B8Fh,3BC5h,3BFFh,3C3Ch,3C7Ch ; 96 entries
; ---------------------------------------------------------------------------
;zbyte_FD8h
zSFXPriority:

If you compare it with the old tables, you will see that zPSGFrequencies gained a couple of new entries; it is because of these new entries that we had to move all this stuff around. Also, the sliding sound uses these entries, which do not exist in the Sonic 2 driver; so it should now be fixed.

SCHG How-To Guide: Sonic the Hedgehog 2 (16-bit)
Fixing Bugs
Fix Demo Playback | Fix a Race Condition with Pattern Load Cues | Fix Super Sonic Bugs | Use Correct Height When Roll Jumping | Fix Jump Height Bug When Exiting Water | Fix Spin Dash Code and Add Spin Dash Speeds | Fix Screen Boundary Spin Dash Bug | Correct Drowning Bugs | Fix Camera Y Position for Tails | Fix Tails Subanimation Error | Fix Tails' Respawn Speeds | Fix Accidental Deletion of Scattered Rings | Fix Ring Timers | Fix Rexon Crash | Fix Monitor Collision Bug | Fix EHZ Deformation Bug | Correct CPZ Boss Attack Behavior | Fix Bug in ARZ Boss Arrow's Platform Behavior | Fix ARZ Boss Walking on Air Glitch | Fix ARZ Boss Sprite Behavior | Fix Multiple CNZ Boss Bugs | Fix HTZ Background Scrolling Mountains | Fix OOZ Launcher Speed Up Glitch | Fix DEZ Giant Mech Collision Glitch | Fix Boss Deconstruction Behavior | Fix Speed Bugs
Design Choices
Remove the Air Speed Cap | Disable Floor Collision While Dying | Modify Super Sonic Transformation Methods & Behavior | Enable/Disable Tails in Certain Levels | Collide with Water After Being Hurt | Retain Rings When Returning at a Star Post | Improve the Fade In\Fade Out Progression Routines | Fix Scattered Rings' Underwater Physics | Insert LZ Water Ripple Effect | Restore Lost CPZ Boss Feature | Prevent SCZ Tornado Spin Dash Death | Improve ObjectMove Subroutines | Port S3K Rings Manager | Port S3K Object Manager | Port S3K Priority Manager | Edit Level Order with ASM‎ | Alter Ring Requirements in Special Stages | Make Special Stage Characters Use Normal DPLCs | Speed Up Ring Loss Process
Adding Features
Create Insta-kill and High Jump Monitors | Create Clone and Special Stage Monitors | Port Knuckles
Sound Features
Port Sonic 1 Sound Driver | Port Sonic 2 Clone Driver | Port Sonic 3 Sound Driver | Expand the Music Index to Start at $00 (Sonic 2 Clone Driver Version)
Extending the Game
Extend the Level Index Past $10 | Extend the Level Select | Extend Water Tables | Add Extra Characters | Free Up 2 Universal SSTs