Introduction
Rather than update CHARS, as is way more common, this print routine utilises UDG to handle printing two characters into a single character block.
It’s a fairly smart routine, and uses a byte which it flips the first bit on and off to determine the printing position of the current character.
Routine: 40 Column Text
Flags: Shift Letter
; Flag: Shift Letter
;
; Will be either $00 or $01, used to track what point the 40 column
; printing is at (if the code needs to print, or shift the lettering).
;
; Used by the routine at Print40ColumnText.
@label=FlagShiftLetter
g$F831 DEFB $00
Buffer: Small Custom Font
; Buffer: Small Custom Font
;
; This is a buffer where the letter doubles are merged together for
; printing to the screen.
;
; Used by the routine at Print40ColumnText.
@label=BufferSmallFont
g$F832 DEFB $00,$00,$00,$00,$00,$00,$00,$00 ; #UDG(#PC)
Table: The Small Custom Font
A little meaningless as it’s just data, but these are the graphic bytes for the small font:
; Small Custom Font
;
; Used by the routine at Print40ColumnText.
;
; This is ASCII "SPACE" ($20), and everything below leads on from here (as
; ASCII).
@label=SmallCustomFont
b$F83A DEFB $00,$00,$00,$00,$00,$00,$00,$00 ; #UDG(#PC)
$F842 DEFB $00,$40,$40,$40,$40,$00,$40,$00 ; #UDG(#PC)
$F84A DEFB $A0,$A0,$00,$00,$00,$00,$00,$00 ; #UDG(#PC)
$F852 DEFB $00,$00,$A0,$E0,$A0,$E0,$A0,$00 ; #UDG(#PC)
$F85A DEFB $40,$E0,$C0,$E0,$60,$E0,$40,$00 ; #UDG(#PC)
$F862 DEFB $00,$00,$A0,$20,$40,$80,$A0,$00 ; #UDG(#PC)
$F86A DEFB $00,$40,$A0,$40,$A0,$C0,$60,$00 ; #UDG(#PC)
$F872 DEFB $00,$40,$40,$00,$00,$00,$00,$00 ; #UDG(#PC)
$F87A DEFB $00,$60,$80,$80,$80,$80,$60,$00 ; #UDG(#PC)
$F882 DEFB $00,$C0,$20,$20,$20,$20,$C0,$00 ; #UDG(#PC)
$F88A DEFB $00,$00,$A0,$40,$E0,$40,$A0,$00 ; #UDG(#PC)
$F892 DEFB $00,$00,$00,$40,$E0,$40,$00,$00 ; #UDG(#PC)
$F89A DEFB $00,$00,$00,$00,$00,$00,$20,$60 ; #UDG(#PC)
$F8A2 DEFB $00,$00,$00,$00,$E0,$00,$00,$00 ; #UDG(#PC)
$F8AA DEFB $00,$00,$00,$00,$00,$00,$40,$00 ; #UDG(#PC)
$F8B2 DEFB $00,$20,$20,$40,$40,$80,$80,$00 ; #UDG(#PC)
$F8BA DEFB $00,$40,$A0,$A0,$A0,$A0,$40,$00 ; #UDG(#PC)
$F8C2 DEFB $00,$40,$C0,$40,$40,$40,$E0,$00 ; #UDG(#PC)
$F8CA DEFB $00,$40,$A0,$20,$40,$80,$E0,$00 ; #UDG(#PC)
$F8D2 DEFB $00,$E0,$20,$60,$20,$A0,$E0,$00 ; #UDG(#PC)
$F8DA DEFB $00,$80,$80,$C0,$E0,$40,$40,$00 ; #UDG(#PC)
$F8E2 DEFB $00,$E0,$80,$C0,$20,$A0,$C0,$00 ; #UDG(#PC)
$F8EA DEFB $00,$60,$80,$C0,$A0,$A0,$40,$00 ; #UDG(#PC)
$F8F2 DEFB $00,$E0,$20,$20,$40,$80,$80,$00 ; #UDG(#PC)
$F8FA DEFB $00,$40,$A0,$40,$A0,$A0,$40,$00 ; #UDG(#PC)
$F902 DEFB $00,$40,$A0,$60,$20,$A0,$40,$00 ; #UDG(#PC)
$F90A DEFB $00,$00,$00,$40,$00,$40,$00,$00 ; #UDG(#PC)
$F912 DEFB $00,$00,$00,$00,$20,$00,$20,$60 ; #UDG(#PC)
$F91A DEFB $00,$00,$20,$40,$80,$40,$20,$00 ; #UDG(#PC)
$F922 DEFB $00,$00,$00,$E0,$00,$E0,$00,$00 ; #UDG(#PC)
$F92A DEFB $00,$00,$80,$40,$20,$40,$80,$00 ; #UDG(#PC)
$F932 DEFB $00,$40,$A0,$20,$40,$00,$40,$00 ; #UDG(#PC)
$F93A DEFB $00,$00,$60,$A0,$E0,$C0,$60,$00 ; #UDG(#PC)
$F942 DEFB $00,$E0,$A0,$A0,$E0,$A0,$A0,$00 ; #UDG(#PC)
$F94A DEFB $00,$E0,$A0,$C0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$F952 DEFB $00,$E0,$80,$80,$80,$80,$E0,$00 ; #UDG(#PC)
$F95A DEFB $00,$C0,$A0,$A0,$A0,$A0,$C0,$00 ; #UDG(#PC)
$F962 DEFB $00,$E0,$80,$C0,$80,$80,$E0,$00 ; #UDG(#PC)
$F96A DEFB $00,$E0,$80,$C0,$80,$80,$80,$00 ; #UDG(#PC)
$F972 DEFB $00,$E0,$80,$E0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$F97A DEFB $00,$A0,$A0,$E0,$A0,$A0,$A0,$00 ; #UDG(#PC)
$F982 DEFB $00,$E0,$40,$40,$40,$40,$E0,$00 ; #UDG(#PC)
$F98A DEFB $00,$E0,$20,$20,$20,$20,$C0,$00 ; #UDG(#PC)
$F992 DEFB $00,$A0,$A0,$C0,$A0,$A0,$A0,$00 ; #UDG(#PC)
$F99A DEFB $00,$80,$80,$80,$80,$80,$E0,$00 ; #UDG(#PC)
$F9A2 DEFB $00,$A0,$E0,$E0,$A0,$A0,$A0,$00 ; #UDG(#PC)
$F9AA DEFB $00,$A0,$E0,$E0,$E0,$E0,$A0,$00 ; #UDG(#PC)
$F9B2 DEFB $00,$E0,$A0,$A0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$F9BA DEFB $00,$E0,$A0,$E0,$80,$80,$80,$00 ; #UDG(#PC)
$F9C2 DEFB $00,$E0,$A0,$A0,$A0,$E0,$E0,$20 ; #UDG(#PC)
$F9CA DEFB $00,$E0,$A0,$C0,$A0,$A0,$A0,$00 ; #UDG(#PC)
$F9D2 DEFB $00,$E0,$80,$E0,$20,$20,$E0,$00 ; #UDG(#PC)
$F9DA DEFB $00,$E0,$40,$40,$40,$40,$40,$00 ; #UDG(#PC)
$F9E2 DEFB $00,$A0,$A0,$A0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$F9EA DEFB $00,$A0,$A0,$A0,$A0,$A0,$40,$00 ; #UDG(#PC)
$F9F2 DEFB $00,$A0,$A0,$A0,$E0,$E0,$A0,$00 ; #UDG(#PC)
$F9FA DEFB $00,$A0,$E0,$40,$E0,$A0,$A0,$00 ; #UDG(#PC)
$FA02 DEFB $00,$A0,$A0,$E0,$40,$40,$40,$00 ; #UDG(#PC)
$FA0A DEFB $00,$E0,$20,$40,$80,$80,$E0,$00 ; #UDG(#PC)
$FA12 DEFB $00,$E0,$80,$80,$80,$80,$E0,$00 ; #UDG(#PC)
$FA1A DEFB $00,$80,$80,$40,$40,$20,$20,$00 ; #UDG(#PC)
$FA22 DEFB $00,$E0,$20,$20,$20,$20,$E0,$00 ; #UDG(#PC)
$FA2A DEFB $00,$40,$E0,$40,$40,$40,$40,$00 ; #UDG(#PC)
$FA32 DEFB $00,$00,$00,$00,$00,$00,$00,$E0 ; #UDG(#PC)
$FA3A DEFB $00,$40,$A0,$80,$E0,$40,$E0,$00 ; #UDG(#PC)
$FA42 DEFB $00,$00,$00,$60,$A0,$A0,$60,$00 ; #UDG(#PC)
$FA4A DEFB $00,$80,$80,$E0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$FA52 DEFB $00,$00,$00,$E0,$80,$80,$E0,$00 ; #UDG(#PC)
$FA5A DEFB $00,$20,$20,$E0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$FA62 DEFB $00,$00,$00,$E0,$A0,$C0,$E0,$00 ; #UDG(#PC)
$FA6A DEFB $00,$60,$80,$C0,$80,$80,$80,$00 ; #UDG(#PC)
$FA72 DEFB $00,$00,$00,$E0,$A0,$E0,$20,$C0 ; #UDG(#PC)
$FA7A DEFB $00,$80,$80,$E0,$A0,$A0,$A0,$00 ; #UDG(#PC)
$FA82 DEFB $00,$40,$00,$40,$40,$40,$40,$00 ; #UDG(#PC)
$FA8A DEFB $00,$20,$00,$E0,$20,$20,$A0,$40 ; #UDG(#PC)
$FA92 DEFB $00,$80,$A0,$C0,$A0,$A0,$A0,$00 ; #UDG(#PC)
$FA9A DEFB $00,$40,$40,$40,$40,$40,$20,$00 ; #UDG(#PC)
$FAA2 DEFB $00,$00,$00,$A0,$E0,$E0,$A0,$00 ; #UDG(#PC)
$FAAA DEFB $00,$00,$00,$E0,$A0,$A0,$A0,$00 ; #UDG(#PC)
$FAB2 DEFB $00,$00,$00,$E0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$FABA DEFB $00,$00,$00,$E0,$A0,$E0,$80,$80 ; #UDG(#PC)
$FAC2 DEFB $00,$00,$00,$E0,$A0,$E0,$20,$20 ; #UDG(#PC)
$FACA DEFB $00,$00,$00,$A0,$C0,$80,$80,$00 ; #UDG(#PC)
$FAD2 DEFB $00,$00,$00,$E0,$C0,$20,$E0,$00 ; #UDG(#PC)
$FADA DEFB $00,$40,$40,$E0,$40,$40,$20,$00 ; #UDG(#PC)
$FAE2 DEFB $00,$00,$00,$A0,$A0,$A0,$E0,$00 ; #UDG(#PC)
$FAEA DEFB $00,$00,$00,$A0,$A0,$A0,$40,$00 ; #UDG(#PC)
$FAF2 DEFB $00,$00,$00,$A0,$A0,$E0,$A0,$00 ; #UDG(#PC)
$FAFA DEFB $00,$00,$00,$A0,$40,$A0,$A0,$00 ; #UDG(#PC)
$FB02 DEFB $00,$00,$00,$A0,$E0,$40,$C0,$00 ; #UDG(#PC)
$FB0A DEFB $00,$00,$00,$E0,$20,$40,$E0,$00 ; #UDG(#PC)
$FB12 DEFB $60,$40,$40,$80,$40,$40,$60,$00 ; #UDG(#PC)
$FB1A DEFB $40,$40,$40,$00,$40,$40,$40,$00 ; #UDG(#PC)
$FB22 DEFB $C0,$40,$40,$20,$40,$40,$C0,$00 ; #UDG(#PC)
$FB2A DEFB $00,$00,$50,$A0,$00,$00,$00,$00 ; #UDG(#PC)
$FB32 DEFB $00,$00,$60,$90,$B0,$90,$60,$00 ; #UDG(#PC)
More useful output is the UDGs themselves, note how they’re all rendered to the left-hand side of the character block.
Address | Font UDG |
---|---|
$F83A | |
$F842 | |
$F84A | |
$F852 | |
$F85A | |
$F862 | |
$F86A | |
$F872 | |
$F87A | |
$F882 | |
$F88A | |
$F892 | |
$F89A | |
$F8A2 | |
$F8AA | |
$F8B2 | |
$F8BA | |
$F8C2 | |
$F8CA | |
$F8D2 | |
$F8DA | |
$F8E2 | |
$F8EA | |
$F8F2 | |
$F8FA | |
$F902 | |
$F90A | |
$F912 | |
$F91A | |
$F922 | |
$F92A | |
$F932 | |
$F93A | |
$F942 | |
$F94A | |
$F952 | |
$F95A | |
$F962 | |
$F96A | |
$F972 | |
$F97A | |
$F982 | |
$F98A | |
$F992 | |
$F99A | |
$F9A2 | |
$F9AA | |
$F9B2 | |
$F9BA | |
$F9C2 | |
$F9CA | |
$F9D2 | |
$F9DA | |
$F9E2 | |
$F9EA | |
$F9F2 | |
$F9FA | |
$FA02 | |
$FA0A | |
$FA12 | |
$FA1A | |
$FA22 | |
$FA2A | |
$FA32 | |
$FA3A | |
$FA42 | |
$FA4A | |
$FA52 | |
$FA5A | |
$FA62 | |
$FA6A | |
$FA72 | |
$FA7A | |
$FA82 | |
$FA8A | |
$FA92 | |
$FA9A | |
$FAA2 | |
$FAAA | |
$FAB2 | |
$FABA | |
$FAC2 | |
$FACA | |
$FAD2 | |
$FADA | |
$FAE2 | |
$FAEA | |
$FAF2 | |
$FAFA | |
$FB02 | |
$FB0A | |
$FB12 | |
$FB1A | |
$FB22 | |
$FB2A | |
$FB32 |
The Routine: 40 Column Text
As already noted, we start by declaring our custom buffer is to be used by UDG.
; Print 40 Column Text
;
; -----------------------------------
; | Input |
; -----------------------------------
; | BC | Length of string to print |
; | DE | Pointer to string to print |
; -----------------------------------
;
@label=Print40ColumnText
c$F7CA LD HL,$F832 ; {Write BufferSmallFont to *UDG.
$F7CD LD ($5C7B),HL ; }
BC holds the length of the string being printed, so here we’re checking if there are still characters left to print - and if there are, the routine jumps to Process40ColumnText.
@label=Print40ColumnText_Loop
*$F7D0 LD A,B ; {Jump to Process40ColumnText if BC is not zero.
$F7D1 OR C ;
$F7D2 JR NZ,$F7E0 ; }
So the length is now zero … but there could still be unprinted data in the buffer just due to how the routine works, so one last final print check before returning. Ordinarily, this wouldn’t ever be a problem as each letter would be printed independently, but here, as we “flip-flop” we’re only printing the buffer when it’s “full” (populated with two characters) - so at this point, on return, if the buffer still contains anything - it needs to be printed to the screen.
$F7D4 LD HL,$F831 ; HL=FlagShiftLetter.
$F7D7 BIT 0,(HL) ; {Just return if the buffer is empty, there's nothing further to
$F7D9 RET Z ; print.}
$F7DA LD A,$90 ; {Print the UDG buffer to the screen.
$F7DC RST $10 ; }
$F7DD RES 0,(HL) ; Reset the buffer flag for the next time this routine is called.
$F7DF RET ; Return.
There are characters to print - so let’s process them! First we check if this is a printable character or a control character such as setting INK or PAPER.
; Process the current letter.
@label=Process40ColumnText
*$F7E0 LD A,(DE) ; Fetch the current letter from the string pointer.
$F7E1 SUB $20 ; Subtract $20 from A to "normalise" it. ASCII characters start
; from $20 ("SPACE") so this makes them $00-based.
$F7E3 JR NC,$F7EE ; Jump to Process40ColumnCharacter if A is equal to or higher
; than zero.
Handle setting a control character by just using standard ASCII handling, and then just loop round for the next character to process.
; Where A is less than zero, this means a control character needs to
; be actioned (e.g. setting INK/ PAPER/ PRINT AT/ etc).
$F7E5 ADD A,$20 ; Add $20 to the character to restore the "proper" ASCII
; value.
$F7E7 PUSH DE ; Temporarily stash the string pointer on the stack.
$F7E8 RST $10 ; Action the current ASCII character/ action.
$F7E9 POP DE ; Restore the string pointer from the stack.
$F7EA INC DE ; Move onto the next letter in the string.
$F7EB DEC BC ; Decrease the string length counter by one.
$F7EC JR $F7D0 ; Jump back to Print40ColumnText_Loop.
This is a genuine letter character, so find the appropriate UDG data in order to prepare for printing it:
; We have a character we want to process, the UDGs for the characters begin at
; SmallCustomFont.
@label=Process40ColumnCharacter
*$F7EE INC A ; Increment A by one.
$F7EF PUSH DE ; {Stash the string pointer and string length on the
$F7F0 PUSH BC ; stack.}
; Locate the UDG character for the current letter.
$F7F1 LD HL,$F832 ; HL=BufferSmallFont.
$F7F4 LD DE,$0008 ; {Keep adding $08 to HL whilst decreasing A by one
@label=LocateSmallCharacter_Loop
*$F7F7 ADD HL,DE ; until A is zero.
$F7F8 DEC A ; }
$F7F9 JR NZ,$F7F7 ; Keep jumping back to LocateSmallCharacter_Loop until
; the correct UDG is located.
HL now contains a pointer to the UDG character of the letter to be printed.
Next, we need to deduce if this character is supposed to be printed to the left or, if we need to shift it over to the right-hand side next to an existing character.
$F7FB LD DE,$F832 ; DE=BufferSmallFont.
; The UDGs are still 8 bytes each, just for each only the first $04
; bits are used of the $08 total bits.
$F7FE LD B,$08 ; Set a UDG counter of $08.
@label=SmallCharacterCopy_Loop
*$F800 LD A,($F831) ; {Test bit 0 of *FlagShiftLetter...
$F803 BIT 0,A ; }
$F805 LD A,(HL) ; Fetch the UDG letter byte.
$F806 JR Z,$F812 ; Jump to WriteSmallCharacterToBuffer if the buffer already has a letter in it.
; Shift the fetched letter from the left-hand side of the UDG buffer over to the
; right-hand side.
$F808 LD C,$04 ; {Shift the bits of the current letter right four bits (move the
@label=ShiftToRightHandSide_Loop
*$F80A SRL A ; letter from being on the left-hand side over to the right-hand side).
$F80C DEC C ;
$F80D JR NZ,$F80A ; }
$F80F LD C,A ; {Merge the current byte in BufferSmallFont together with the shifted letter.
$F810 LD A,(DE) ;
$F811 OR C ; }
Update the buffer.
@label=WriteSmallCharacterToBuffer
*$F812 LD (DE),A ; Write A to the buffer.
$F813 INC HL ; Move onto the next byte of the UDG letter.
$F814 INC DE ; Move onto the next byte in the buffer.
$F815 DJNZ $F800 ; Decrease the UDG byte counter by one and loop back to
; SmallCharacterCopy_Loop until the counter is zero.
Now, as previously mentioned, the buffer isn’t always printed to the screen, only when it’s a double, else we just loop around again.
; This toggle is the "magic" - it handles the left/ right flow of the code.
$F817 LD A,($F831) ; {Flip bit 0 of *FlagShiftLetter.
$F81A XOR %00000001 ;
$F81C LD ($F831),A ; }
$F81F POP BC ; {Restore the string length and string pointer from the
$F820 POP DE ; stack.}
$F821 INC DE ; Move onto the next letter in the string.
$F822 DEC BC ; Decrease the string length counter by one.
$F823 LD A,($F831) ; {Jump to Print40ColumnText_Loop if *FlagShiftLetter states that the buffer doesn't
$F826 BIT 0,A ; have both a left and right half yet.
$F828 JR NZ,$F7D0 ; }
The buffer has two characters in it, so print it to the screen and continue looping around for the next character(s).
; Else, we can now print the UDG buffer at BufferSmallFont to the screen.
$F82A PUSH DE ; Temporarily stash the string pointer on the stack.
$F82B LD A,$90 ; {Print the UDG buffer to the screen.
$F82D RST $10 ; }
$F82E POP DE ; Restore the string pointer from the stack.
$F82F JR $F7D0 ; Jump to Print40ColumnText_Loop.
Result
Test Data
; Message Block: $04
;
; Message: $01.
@label=MessageBlock_04
t$825B DEFM " You won the "
$8269 DEFM " battle, but "
$8277 DEFM " not the war! "
$8285 DEFM " "
; Message: $02.
$8291 DEFM " "
$829F DEFM " You won ! "
$82AD DEFM " "
$82BB DEFM " "
; Message: $03.
$82C7 DEFM "unreal cards! "
$82D5 DEFM "you better not"
$82E3 DEFM " be cheating! "
$82F1 DEFM " "
; Message: $04.
$82FD DEFM " I lost this "
$830B DEFM "time, but look"
$8319 DEFM "out next time!"
$8327 DEFM " "
; Message: $05.
$8333 DEFM " "
$8341 DEFM " I Lost this "
$834F DEFM " round! "
$835D DEFM " "
Output
Noting that the black space is because it’s rendered as a speech bubble and that is the bottom corner of it.