CD Centiseconds HUD - RetroKoH/S1Fixed GitHub Wiki

(Original guide by Mercury)
Source: ReadySonic
Original Commit: 07d7a88
This mod can be toggled in S1Fixed by setting HUDCentiseconds to 0 or 1.

With the exception of Sonic CD, every Sonic game denotes time elapsed in the format of M:SS, but Sonic CD adds centiseconds to the HUD timer. S1Fixed applies this to Special Stage HUDs as well, but this guide will only cover the basic HUD for now, with an addendum added in the future. First things first, let's open Variables.asm and add a RAM variable that will serve as a value with which to increment centiseconds. In stock Sonic 1, there are 4 RAM bytes related to time, though one of them is unused:

v_time:				ds.l	1	; time (First byte is unused)
v_timemin = v_time+1				; time - minutes
v_timesec = v_time+2				; time - seconds
v_timecent = v_time+3				; time - centiseconds

We need to find a free byte of RAM for centisecond incrementing. Find a free byte of RAM and declare it like so.

v_centstep:			ds.b	1	; value used to increment centiseconds

Now, let's go to _inc/HUD_Update.asm. We have some changes to make to the updating routine. Go to the label .chktime, and replace this:

	cmpi.l	#(9*$10000)+(59*$100)+59,(a1)+	; is the time 9:59:59?
	beq.s	TimeOver			; if yes, branch

	addq.b	#1,-(a1)			; increment 1/60s counter
	cmpi.b	#60,(a1)			; check if passed 60
	blo.s	.chklives

With this:

	cmpi.l	#(9*$10000)+(59*$100)+99,(a1)+	; is the time 9:59:99?
	beq.w	TimeOver			; if yes, branch

	move.b	(v_centstep).w,d1
	addi.b	#1,d1				; increment step counter
	cmpi.b	#3,d1
	bne.s	.skip
	move.b	#0,d1

.skip:
	move.b	d1,(v_centstep).w
	cmpi.b	#2,d1
	beq.s	.skip2
	addi.b	#1,d1

.skip2:
	add.b	d1,-(a1)			; increment centisecond timer using v_centstep
	cmpi.b	#100,(a1)			; check if passed 100
	bcs.s	.docent

In the original code, the v_timecent only counted up to 60, which makes sense. There was nothing displayed for centiseconds so there was no need to count actual centiseconds, and much like how there are 100 centiseconds in a second, there are (roughly) 60 frames in one second of gameplay. With this mod, however, we need to actually count and display centiseconds, but we can't count straight from 0 to 100 every second, given the number of frames possible per second. Therefore, the mod uses the new v_centstep to increment centiseconds, which now need to be displayed on screen. This can emulate centiseconds, allowing us to display them in the timer.

Now we need to update the graphics in VRAM so the centiseconds can actually appear. This guide is going to assume that we place the new centisecond art in VRAM location $F400. For this guide, I'll represent this address with the name locVRAM_Centi. You might decide to, or need to, load the art elsewhere. Change the value in the locVRAM macro accordingly. At the end of .updatetime, we are going to add code to load art for the centiseconds like so:

	moveq	#0,d1
	move.b	(v_timesec).w,d1 	; load seconds
	bsr.w	Hud_Secs

+.docent:
+	locVRAM	locVRAM_Centi+40,d0	; $F440
+	moveq	#0,d1
+	move.b	(v_timecent).w,d1	; load centiseconds
+	bsr.w	Hud_Secs

.chklives:
	tst.b	(f_lifecount).w ; does the lives counter need updating?

Now, scroll down to the subroutine Hud_LoadZero. Just after it, we are going to load a subroutine to load the second (') and centisecond (") marks. It looks like this:

; ---------------------------------------------------------------------------
; Subroutine to	load " on the HUD
; ---------------------------------------------------------------------------

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


Hud_LoadMarks:
		locVRAM	locVRAM_Centi
		lea	Hud_TilesMarks(pc),a2
		move.w	#2,d2
		bra.s	loc_1C83E
; End of function Hud_LoadMarks

As you can see from the second line, we load a pointer to data labelled Hud_TilesMarks to register a2. The problem is, there is no data with that label. We're gonna fix that, but before we do, we need to make sure we actually call Hud_LoadMarks. We'll do that at Hud_Base. Let's add it here:

Hud_Base:
	lea	(vdp_data_port).l,a6
	bsr.w	Hud_Lives
+	bsr.s	Hud_LoadMarks

After the end of Hud_Base, you'll see a snippet of data used for loading art for the HUD:

	Hud_TilesBase:	dc.b $16, $FF, $FF, $FF, $FF, $FF, $FF,	0, 0, $14, 0, 0
	Hud_TilesZero:	dc.b $FF, $FF, 0, 0

We need to add Hud_TilesMarks, and slightly alter Hud_TilesBase:

+	Hud_TilesMarks:	dc.b $1A, 0, 0, 0
!	Hud_TilesBase:	dc.b $16, $FF, $FF, $FF, $FF, $FF, $FF,	0, 0, $18, 0, 0
	Hud_TilesZero:	dc.b $FF, $FF, 0, 0

Now, we need to add files for the new art. You can take the uncompressed art tile .bin file here. The new mappings file is here:

; ---------------------------------------------------------------------------
; Sprite mappings - SCORE, TIME, RINGS (Mercury HUD Centiseconds)
; ---------------------------------------------------------------------------
Map_HUD_internal:	mappingsTable
	mappingsTableEntry.w	.allyellow
	mappingsTableEntry.w	.ringred
	mappingsTableEntry.w	.timered
	mappingsTableEntry.w	.allred

; spritePiece format:
; left position, top position, width (in tiles), height (in tiles), tile offset, xflip, yflip, palette line, priority flag

.allyellow:	spriteHeader
	spritePiece	0, -$80, 4, 2, 0, 0, 0, 0, 1		; SCOR
	spritePiece	$20, -$80, 4, 2, $16, 0, 0, 0, 1	; E ###
	spritePiece	$40, -$80, 4, 2, $1E, 0, 0, 0, 1	; ####
	spritePiece	0, -$70, 4, 2, $E, 0, 0, 0, 1		; TIME
	spritePiece	$28, -$70, 4, 2, $26, 0, 0, 0, 1	; #'##
	spritePiece	$48, -$70, 3, 2, $D6, 0, 0, 0, 1	; "##
	spritePiece	0, -$60, 4, 2, 6, 0, 0, 0, 1		; RING
	spritePiece	$20, -$60, 1, 2, 0, 0, 0, 0, 1		; S
	spritePiece	$30, -$60, 3, 2, $2E, 0, 0, 0, 1	; ###
	spritePiece	0, $40, 2, 2, $10A, 0, 0, 0, 1		; Lives Icon
	spritePiece	$10, $40, 4, 2, $10E, 0, 0, 0, 1	; Lives x ##
.allyellow_End
	even

.ringred:	spriteHeader
	spritePiece	0, -$80, 4, 2, 0, 0, 0, 0, 1		; SCOR
	spritePiece	$20, -$80, 4, 2, $16, 0, 0, 0, 1	; E ###
	spritePiece	$40, -$80, 4, 2, $1E, 0, 0, 0, 1	; ####
	spritePiece	0, -$70, 4, 2, $E, 0, 0, 0, 1		; TIME
	spritePiece	$28, -$70, 4, 2, $26, 0, 0, 0, 1	; #'##
	spritePiece	$48, -$70, 3, 2, $D6, 0, 0, 0, 1	; "##
	spritePiece	0, -$60, 4, 2, 6, 0, 0, 1, 1		; RING
	spritePiece	$20, -$60, 1, 2, 0, 0, 0, 1, 1		; S
	spritePiece	$30, -$60, 3, 2, $2E, 0, 0, 0, 1	; ###
	spritePiece	0, $40, 2, 2, $10A, 0, 0, 0, 1		; Lives Icon
	spritePiece	$10, $40, 4, 2, $10E, 0, 0, 0, 1	; Lives x ##
.ringred_End
	even

.timered:	spriteHeader
	spritePiece	0, -$80, 4, 2, 0, 0, 0, 0, 1		; SCOR
	spritePiece	$20, -$80, 4, 2, $16, 0, 0, 0, 1	; E ###
	spritePiece	$40, -$80, 4, 2, $1E, 0, 0, 0, 1	; ####
	spritePiece	0, -$70, 4, 2, $E, 0, 0, 1, 1		; TIME
	spritePiece	$28, -$70, 4, 2, $26, 0, 0, 0, 1	; #'##
	spritePiece	$48, -$70, 3, 2, $D6, 0, 0, 0, 1	; "##
	spritePiece	0, -$60, 4, 2, 6, 0, 0, 0, 1		; RING
	spritePiece	$20, -$60, 1, 2, 0, 0, 0, 0, 1		; S
	spritePiece	$30, -$60, 3, 2, $2E, 0, 0, 0, 1	; ###
	spritePiece	0, $40, 2, 2, $10A, 0, 0, 0, 1		; Lives Icon
	spritePiece	$10, $40, 4, 2, $10E, 0, 0, 0, 1	; Lives x ##
.timered_End
	even

.allred:	spriteHeader
	spritePiece	0, -$80, 4, 2, 0, 0, 0, 0, 1		; SCOR
	spritePiece	$20, -$80, 4, 2, $16, 0, 0, 0, 1	; E ###
	spritePiece	$40, -$80, 4, 2, $1E, 0, 0, 0, 1	; ####
	spritePiece	0, -$70, 4, 2, $E, 0, 0, 1, 1		; TIME
	spritePiece	$28, -$70, 4, 2, $26, 0, 0, 0, 1	; #'##
	spritePiece	$48, -$70, 3, 2, $D6, 0, 0, 0, 1	; "##
	spritePiece	0, -$60, 4, 2, 6, 0, 0, 1, 1		; RING
	spritePiece	$20, -$60, 1, 2, 0, 0, 0, 1, 1		; S
	spritePiece	$30, -$60, 3, 2, $2E, 0, 0, 0, 1	; ###
	spritePiece	0, $40, 2, 2, $10A, 0, 0, 0, 1		; Lives Icon
	spritePiece	$10, $40, 4, 2, $10E, 0, 0, 0, 1	; Lives x ##
.allred_End
	even

These mappings use MainMemory's MapMacro format. This allows them to be used with any of the 3 Sonic games, as well as Flex2. as you can see towards the top of the given file, the 5th item in the spritePiece macro is the tile offset. You'll want to adjust these values based on what's in your disassembly.

Finally, we need to go to sonic.asm to make a few changes. First, go to Level_LoadObj. You'll notice that rings and time are being cleared. We also need to clear v_centstep here:

Level_LoadObj:
	jsr	(ObjPosLoad).l
	jsr	(ExecuteObjects).l
	jsr	(BuildSprites).l
	tst.b	(v_lastlamp).w		; are you starting from	a lamppost?
	bne.s	Level_SkipClr		; if yes, branch
	clr.w	(v_rings).w		; clear rings
	clr.l	(v_time).w		; clear time
+	clr.b	(v_centstep).w
	clr.b	(v_lifecount).w		; clear lives counter

Make sure to jump down to Cont_GotoLevel and clear v_centstep there also.

At Level_SkipClr, we need to delete this line, as we are going to place this elsewhere:

	move.b	#1,(f_timecount).w	; update time counter

And here is "elsewhere"! At Level_StartGame, place this immediately under .demo:

.demo:
+	move.b	#1,(f_timecount).w	; update time counter
	bclr	#7,(v_gamemode).w	; subtract $80 from mode to end pre-level stuff

Finally, make sure the art and mappings files I provided are "artunc/HUD Numbers.bin" and "_maps/HUD.asm". Aside from potential issues w/ tile offsets and VRAM locations that I mentioned earlier, which will be dependent on how you have VRAM set up in your project, you should be all set! Enjoy!