Pobtastic / 180 / Elegant Print Handling

Created Wed, 13 Nov 2024 09:56:09 +0100 Modified Thu, 12 Dec 2024 14:35:47 +0000

Introduction

When I first tried disassembling 180, I was confused with the games entrypoint. It looked to be … nonsense! Usually, this is indicative of subsequent code decrypting the instructions you’re looking at (so it’s actually different to what you initially see). But, 180 appeared to have a pattern of sorts … a routine would be called, and later there would be an RST $38 … eventually this gave the game away and helped me to decipher exactly what the code is for, as soon as I realised that the hex value of RST $38 is actually $FF this clue helped me recognise it as a data terminator.

What I’d thought was encrypted code was actually text data terminated with an $FF. It confused me at first, just as I’d never seen anything like it before. It’s such an elegant way to handle printing data that I couldn’t wait to write it up!

Subroutine: Messaging: Bust

; Messaging: Bust
@label=Messaging_Bust
c$AD3B CALL $AC52       ; Call #R$AC52.
 $AD3E CALL $964C       ; Call PrintString_Loop.
 $AD41 DEFB $16,$17,$10 ; PRINT AT: 17, 10.
 $AD44 DEFM "BUST[["    ; #FONT#(:(#STR(#PC,$04,$06)))$8D75,attr=$47(bust)
 $AD4A DEFB $FF         ; Terminator.
 $AD4B CALL $AE04       ; Call Pause_Long.
...etc.

There’s a CALL PrintString_Loop but immediately afterward there is character data! Such a clever idea! And once the terminator is reached, the code just continues on - so elegant!!! I don’t know if other games use this technique as well, but it’s the first time I’ve seen it done like this, and I’m so incredibly impressed at the ingenuity of it! Maybe by 1986 everyone was writing printing routines like this - who knows?!?! But it’s my first time seeing it, and I’m suitably impressed 😆

Subroutine: Print String

This isn’t that the routine is complex - it actually isn’t at all, but it is truly a wonderfully clever idea!

Here is what the print routine looks like:

; Print String
;
; Prints the string data from the stack pointer to the screen buffer.
;
@label=PrintString_Loop
c$964C EX (SP),HL    ; Exchange HL with the address at the top of the
                     ; stack.
 $964D LD A,(HL)     ; Get the character from string.
 $964E INC HL        ; Increment the string pointer by one.
 $964F EX (SP),HL    ; Update the return address on the stack.
 $9650 CP $FF        ; {Return if the terminator has been reached.
 $9652 RET Z         ; }
 $9653 CALL $9736    ; Call PrintCharacter.
 $9656 JR $964C      ; Jump back to PrintString_Loop.

In case you’re not familiar with how the Z80 CPU works; the program counter (PC) holds the memory address of the current instruction being executed. When a subroutine is CALLed, the processor adds 3 to the program counter and pushes it onto the stack (3 bytes being the length of a CALL instruction).

When the RETurn from the subroutine is issued, the program counter is updated with the popped address from the stack and so, execution continues from where it left off. You can’t directly modify the PC register … but you can fudge with the values on the stack! This is vital to understanding how this subroutine works and why it’s so special!

So, breaking it down from the Messaging_Bust subroutine above:

 $AD3E CALL $964C       ; Call PrintString_Loop.

At this point; PC is $AD3E and the CPU polls the memory location and has a CALL to action. So, as we’ve learnt above - the processor adds 3 to the program counter (the length of a CALL instruction) and pushes this address onto the stack.

The PrintString_Loop has been called and there’s some magic on the very first line:

@label=PrintString_Loop
c$964C EX (SP),HL    ; Exchange HL with the address at the top of the
                     ; stack.

HL now contains the address $AD41 ($AD3E+$03) and now points at the string data for printing!

Next, it fetches the string data, and advances the return address on the stack:

 $964D LD A,(HL)     ; Get the character from string.
 $964E INC HL        ; Increment the string pointer by one.
 $964F EX (SP),HL    ; Update the return address on the stack.

Then it does a check to see if this is the last character in the string:

 $9650 CP $FF        ; {Return if the terminator has been reached.
 $9652 RET Z         ; }

And if it isn’t, calls the PrintCharacter subroutine:

 $9653 CALL $9736    ; Call PrintCharacter.

Lastly, it simply loops back to the beginning to examine the next character. So, continues to print each character until it hits a terminator:

 $9656 JR $964C      ; Jump back to PrintString_Loop.