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!