Pobtastic / Trashman / Print Numbers

Created Wed, 20 Mar 2024 20:45:54 +0000 Modified Sat, 04 May 2024 21:31:28 +0000

Introduction

Printing strings is easy, you have a string, and you just walk through it and output it. Perhaps you might hardcode the length so you know where to stop, or perhaps there’s a termination character or maybe bit 7 of the last character is set - both indicating the end of the string. Simple!

But, printing numbers on the other hand … if you need it to be, it can be a bit more difficult.

What Does It Do?

These types of routines are fairly common, and it is interesting to compare them to see how developers did similar things in slightly differing ways.

Why I find this interesting is because Trashman does something a little different; it uses the RLD command. This is a fairly seldom seen command because it has a pretty niche effect:

Performs a 4-bit leftward rotation of the 12-bit number whose 4 most significant bits are the 4 least significant bits of A, and its 8 least significant bits are in *HL.

For example; If A contains %aaaaxxxx and *HL is %yyyyzzzz initially, their final values will be A=%aaaayyyy and *HL=%zzzzxxxx.

So yes, you read that correctly! Z80 has a command which rotates bits as if the input is a 12-bit number! It’s used to adjust the binary-coded decimal (BCD) numbers in a very specific way.

Imagine you have the following two numbers:

@label=Start
c$CB0A LD A,$24      ; The accumulator (A) contains the BCD number $24 (hexadecimal).
 $CB0C LD HL,$C272   ; The memory location A_Number contains the BCD number $67 (hexadecimal).

@label=A_Number
g$C272 DEFB $67

Here’s what happens when you execute the RLD instruction:

  1. The $04 from the accumulator becomes the high digit of the BCD number at the memory location HL.
  2. The high digit of the BCD number at HL ($06) moves to the low digit of the accumulator.
  3. The low digit of the BCD number at HL ($07) becomes the low digit at the memory location HL.

So, before RLD, you have:

A = $24
*HL = $67

And after RLD, you will have:

A = $26
*HL = $47

But how does this help when printing numbers?! RLD in this instance is being used to “extract” the numbers independently for printing. It extracts each digit, so it can be displayed - this is why it always starts A off with being $00.

Here’s a more direct example:

---
title: "Extracting Digits: Given $02 And $50 For \"250\""
---
graph TD;
    B1("BCD: 0x02, 0x50") -->|Digit Extraction| B2("Digit: 0")
    B2 -->|To ASCII| B3("ASCII: 'SPACE'")
    B1 -->|Next Digit| B4("Digit: 2")
    B4 -->|To ASCII| B5("ASCII: '2'")
    B1 -->|Next Digit| B6("Digit: 5")
    B6 -->|To ASCII| B7("ASCII: '5'")
    B1 -->|Final Digit| B8("Digit: 0")
    B8 -->|To ASCII| B9("ASCII: '0'")

For a “Real-World” example, say a score digit is $57:

  • Initial A = $00 (%00000000 in binary)
  • Initial *HL = $57 (%01010111 in binary)

After the RLD instruction:

  1. The high nibble of *HL (%0101) is moved to the low nibble of A, resulting in A becoming %00000101 in binary, which is $05 in hexadecimal.
  2. The low nibble of *HL (%0111) moves to the high nibble of *HL, and the original low nibble of A (%0000) moves to the low nibble of *HL. Making *HL %01110000 in binary ($70 in hexadecimal).

So, after the RLD operation:

  • A will contain $05 (%00000101 in binary)
  • *HL will contain $70 (%01110000 in binary)

Then, running a second RLD:

  • A will contain $07 (%00000111 in binary)
  • *HL will contain $05 (%00000101 in binary)

This allows the routine to print each place value to the screen buffer separately.

Note, there is a third RLD operation, and this is for housekeeping as running it again results in:

  • A will contain $00 (%00000000 in binary)
  • *HL will contain $57 (%01010111 in binary)

So, restoring the initial value of *HL.

Subroutine: Print Bonus/ Score

First off, let’s take a look at the subroutine which prints both the bonus and the players score. It simply “sets-up” some parameters and then calls the print numbers routine.

; Print Bonus/ Score
;
; Print the current "Bonus" value.
;
@label=Print_Bonus
c$CB0A LD B,$02      ; B=$02 (number of characters in the bonus).
 $CB0C LD DE,$403B   ; DE=$403B (screen buffer location).
; Work backwards - the data is stored back-to-front e.g. $50,$02 for outputting "250".
 $CB0F LD HL,$C272   ; {HL=Time_Remaining+$01.
 $CB12 INC HL        ; }
 $CB13 CALL $CAB3    ; Call PrintNumbers.

; Print the current score.
 $CB16 LD DE,$4020   ; #REGde=$4020 (screen buffer location).
@label=Print_Score
*$CB19 LD B,$03      ; B=$03 (number of characters in the players score).
 $CB1B LD HL,$CB98   ; HL=the last digit of ActivePlayer_Score.
 $CB1E CALL $CAB3    ; Call PrintNumbers.
 $CB21 RET           ; Return.

The reason we pass the largest digit of the number first is the complication with printing numbers in general. Sure, you can print say 001 for one, or 010 for ten. But, isn’t it a lot nicer looking to just print 1 or 10? In starting with the largest number and evaluating it right-to-left, we then know when to start printing actual numbers and not spacing.

For some context, here’s both sets of data.

Data: Time Remaining

; Time Remaining
;
; Time Remaining equates to how much bonus the player receives when the level is completed.
; Stored as e.g.
; --------------------
; | Location | Value |
; --------------------
; | $C272    | $50   |
; | $C273    | $02   |
; ---------------------
; For "250" seconds/ bonus points remaining.
;
@label=Time_Remaining
g$C272 DEFB $00
 $C273 DEFB $00

Data: Active Players Score

; Active Players Score
;
; The current players score for display.
; Stored as e.g.
; --------------------
; | Location | Value |
; --------------------
; | $CB96    | $33   |
; | $CB97    | $22   |
; | $CB98    | $11   |
; ---------------------
; For a score of "112233" points.
;
@label=ActivePlayer_Score
g$CB96 DEFB $00
 $CB97 DEFB $00
 $CB98 DEFB $00

Subroutine: Print Numbers

The first thing to note, there are two loops;

  • On entry, the first loop handles printing a “space” rather than any numbers;
    • As soon as we hit a number, we move to the second loop
  • The second loop handles printing numbers

It seems obvious writing it here but given the completities of the RLD command, I felt it best to clarify.

; Print Numbers
;
; -----------------------------------------
; | Input                                 |
; -----------------------------------------
; |B |Number of characters to print       |
; |DE|Screen buffer pointer               |
; |HL|Pointer to end of number string data|
; -----------------------------------------
;
@label=PrintNumbers
c$CAB3 LD A,$00      ; A=$00.
 $CAB5 RLD           ; Extract the first digit into the accumulator.
 $CAB7 PUSH AF       ; Stash the result on the stack.
 $CAB8 CP $00        ; {Jump to PrintNumbers_BigNumber if A is not $00.
 $CABA JR NZ,$CADA   ; }

; The highest digit is zero, so instead of printing an ASCII zero, print an ASCII space character instead.
 $CABC LD A,$20      ; Load A with the ASCII space character ($20).
 $CABE CALL $CAEF    ; Call PrintNumbers_Print.

; Move onto the next digit extraction.
 $CAC1 POP AF        ; Restore the accumulator from the stack.
 $CAC2 RLD           ; Extract the next digit into the accumulator.
 $CAC4 PUSH AF       ; Stash the result on the stack.
 $CAC5 CP $00        ; {Jump to PrintNumbers_LittleNumber if A is not $00.
 $CAC7 JR NZ,$CAE3   ; }

; The next digit is also zero, so again print an ASCII space character.
 $CAC9 LD A,$20      ; Load A with the ASCII space character ($20).
 $CACB CALL $CAEF    ; Call PrintNumbers_Print.

; Housekeeping, and loop control.
 $CACE POP AF        ; Restore the accumulator from the stack.
 $CACF RLD           ; Restore the original byte value back to *HL.
 $CAD1 DEC HL        ; Decrease the string pointer by one - move to the next number character.
 $CAD2 DJNZ $CAB3    ; Decrease the length counter by one and loop back to PrintNumbers until
                     ; the counter is zero.
 $CAD4 RET           ; Return.

; Note; the above doesn't jump straight to this, as it's already processing the first RLD result.
@label=PrintNumbers_Loop
*$CAD5 LD A,$00      ; A=$00.
 $CAD7 RLD           ; Extract the first digit into the accumulator.
 $CAD9 PUSH AF       ; Stash the result on the stack.

@label=PrintNumbers_BigNumber
*$CADA ADD A,$30     ; A contains the number character to be printed - add ASCII zero to it ($30).
 $CADC CALL $CAEF    ; Call PrintNumbers_Print.

 $CADF POP AF        ; Restore the accumulator from the stack.
 $CAE0 RLD           ; Extract the next digit into the accumulator.
 $CAE2 PUSH AF       ; Stash the result on the stack.

@label=PrintNumbers_LittleNumber
*$CAE3 ADD A,$30     ; A contains the number character to be printed - add ASCII zero to it ($30).
 $CAE5 CALL $CAEF    ; Call PrintNumbers_Print.

 $CAE8 POP AF        ; Restore the accumulator from the stack.
 $CAE9 RLD           ; Restore the original byte value back to *HL.
 $CAEB DEC HL        ; Decrease the string pointer by one - move to the next number character.
 $CAEC DJNZ $CAD5    ; Decrease the length counter by one and loop back to PrintNumbers_Loop until
                     ; the counter is zero.
 $CAEE RET           ; Return.

; This is similar in functionality to PrintString.
; It differs as it doesn't use a terminator rather the length is known.
@label=PrintNumbers_Print
*$CAEF PUSH HL       ; {Stash the pointer to the string and the length counter on the stack.
 $CAF0 PUSH BC       ; }
; Create an offset in HL using A - which contains the number value we want to print.
 $CAF1 LD L,A        ; {Using the accumulator, calculate the address of the number in the custom font.
 $CAF2 LD H,$00      ; HL=A*$08 (as each font character is made up of 8 bytes).
 $CAF4 ADD HL,HL     ;
 $CAF5 ADD HL,HL     ;
 $CAF6 ADD HL,HL     ; }
 $CAF7 LD BC,$E417   ; BC=Beginning of the custom font.
 $CAFA DEC B         ; Decrease B by one.
 $CAFB ADD HL,BC     ; HL+=Font.
 $CAFC PUSH DE       ; Stash the current screen buffer pointer on the stack.
 $CAFD LD B,$08      ; Create a counter of $08 (because each font character is 8 bytes).
@label=PrintNumbers_Print_Loop
*$CAFF LD A,(HL)     ; Fetch a byte from the custom font.
 $CB00 LD (DE),A     ; Write the byte to the screen buffer.
 $CB01 INC HL        ; Move onto the next byte of the custom font.
 $CB02 INC D         ; Adjust the screen buffer position down one row.
 $CB03 DJNZ $CAFF    ; Decrease the byte counter by one and loop back to PrintNumbers_Print_Loop until
                     ; the counter is zero (all 8 bytes have been copied to the screen buffer).
 $CB05 POP DE        ; Restore the original screen buffer pointer from the stack.
 $CB06 INC DE        ; Move the screen buffer pointer right one character block ready for printing the next character.
 $CB07 POP BC        ; {Restore the length counter and pointer to the string from the stack.
 $CB08 POP HL        ; }
 $CB09 RET           ; Return.

Trashman Disassembly