Pobtastic / Booty / Unpacking Rooms

Created Wed, 10 Apr 2024 07:49:53 +0100 Modified Thu, 12 Dec 2024 14:35:47 +0000

Introduction

When a new game is started, Booty uses two methods for initialisation:

  1. The players lives are set to 4; even though on start the game sets the lives to 3.
  2. CurrentRoom is set to 0; even though there is no room 0.

Both these things happen for “set up” purposes - even though GameState exists and could quite easily have accommodated it.

The reason is, on starting a new game, in a game where it’s expected that when you move between rooms - a “snapshot” of the state of that room is stored in a buffer ready for when you return to it, Booty does this too yet … the table which should point to the start of each rooms data is oddly blank when the game finishes loading.

There’s not really any good reason for this, as the length of the room data won’t ever vary between games - in fact, this table won’t vary ever again no matter how many times you play the game (even though it’s rebuilt regardless on game start).

So why does every reference start at $0000?…

I have a theory! Possibly during game development, John F. Cain, the developer, might have built the game “engine” first, followed by creation of the level layouts afterward - before the game launched. Given the dynamic nature of this pointer table, it’s evident that any modifications to the room data are immediately mirrored in the game whenever a new game is started. This would have allowed him to play-test and tweak here and there very simply. Obviously this is just a theory though, it doesn’t sound like anyone has been able to contact John F. Cain at all over the years, which is a huge shame, some of the games he’s authored are absolute gems!

Routine: New Game

From starting a new game:

; Start Game
;
; When a new game starts, the player lives have already been set to $04 in
; DisplayIntroductionScreen.
;
; *GameState will be set to $02 after the player has collected the golden
; key and the game restarts.
; This is how the game ensures that the lives and booty count are not reset.
;
; Game modes $01 and $02 appear to be mostly identical, however as the
; booty count is retained in the new game - this means the animals will continue
; to appear more frequently as the count checks if the number of items of
; booty is higher than 100. See the check at AnimalsEventTiming.
@label=StartGame
c$D08A LD A,($5BF1)  ; {Jump to SetNormalGame if *PlayerLives is equal to $04.
 $D08D CP $04        ;
 $D08F JR Z,$D099    ; }

; Else the game has been looped already so set *GameState accordingly.
 $D091 LD A,$02      ; {Write "Game Looped Mode" ($02) to *GameState.
 $D093 LD ($5BF0),A  ; }
 $D096 JP $D09E      ; Jump to NewGame.

; This is a normal "new" game.
@label=SetNormalGame
*$D099 LD A,$01      ; {Write "Normal Game" ($01) to *GameState.
 $D09B LD ($5BF0),A  ; }

; Initialise...
@label=NewGame
*$D09E CALL $DEA8    ; Call InitialiseGame.

...further code omitted; not relevant for this article.

That’s probably more code than we need to explain this but regardless, it’s interesting to show how the game states work.

Routine: Initialise Game

The important part to take away from the next routine is that, on a newly initialised game $00 is written to CurrentRoom (regardless of whether this is a “normal” or “looped” game - although that’s not really relevant here). Why this is important is, there is no room $00 - instead this is used for branching the code later on.

; Initialise Game
;
; Don't reset player lives or booty count if the game has already been
; completed and has looped around to start again.
@label=InitialiseGame
c$DEA8 LD A,($5BF0)  ; {Jump to InitialiseGameStates if *GameState is set to "Game Looped
 $DEAB CP $02        ; Mode" ($02).
 $DEAD JP Z,$DEBC    ; }

; Initialise new Game State attributes.
 $DEB0 LD A,$03      ; {Write $03 to *PlayerLives.
 $DEB2 LD ($5BF1),A  ; }
 $DEB5 LD BC,$0000   ; {Write $0000 to *PlayerBooty.
 $DEB8 LD ($5BF4),BC ; }

@label=InitialiseGameStates
*$DEBC XOR A         ; {Write $00 to *CurrentRoom.
 $DEBD LD ($5BD3),A  ; }
 $DEC0 LD BC,$007D   ; {Write #$007D to *PlayerTreasure.
 $DEC3 LD ($5BF2),BC ; }
 $DEC7 JR $DECE      ; Jump to InitialisePlayer.

Controller: Draw Room

Skipping over quite a bit of code, eventually we reach a controller for drawing the room. You can see that there are two routines - and which is called is determined by *CurrentRoom being $00 or not.

; Controller: Draw Room
;
; On a new game, the game starts with the room ID being $00 (which isn't a valid room ID).
; The reason is that it chooses between these two set up routines here
; (and corrects the starting room ID later).
@label=ControllerDrawRoom
c$A804 LD A,($5BD3)  ; {Call UnpackAllRooms if *CurrentRoom is $00.
 $A807 CP $00        ;
 $A809 PUSH AF       ;
 $A80A CALL Z,$AA97  ; }

; Handle all other room IDs.
 $A80D POP AF        ; {Call UnpackRoom if *CurrentRoom was not equal to $00.
 $A80E CALL NZ,$AAF4 ; }

 $A811 CALL $AB44    ; Call PopulateCurrentRoomBuffersAndReferences.
 $A814 JP $A900      ; Jump to DrawRoom.

Subroutine: Unpack All Rooms

The subroutine description here is a little jargon-y so keep this simple flow in mind when following it:

---
title: "Populating The Room Pointer Table"
---
graph TD;
    style A1 fill:#f9f,stroke:#333,stroke-width:4px
    style A2 fill:#f9f,stroke:#333,stroke-width:4px
    style A6 fill:#bbf,stroke:#333,stroke-width:4px
    A1(Initialise Pointer: Start at Room 21) --> A2
    A2(Point to Default Room Data) --> A3
    A3(Write Pointer to TableRoomData) --> A4{Reached Terminator Yet?}
    A4 -->|No| A5(Process Room Data)
    A5 --> A3
    A4 -->|Yes| A6(Complete)
    A4 ~~~ dummy:::hidden ~~~ A6
    classDef hidden display: none;
; Unpack All Rooms
;
; This is similar to UnpackRoom except that instead of copying a single rooms
; data from the rooms data buffer, this routine loops through ALL the rooms -
; copying the default room data into the room data buffers. Also, at the start
; of every loop - it writes the room data buffer address which is about to be
; processed to the room data table as both the table and the room data
; buffers are completely blank after the game first loads.
; .
; Note; this always occurs at the start of every game regardless, the game
; opens with the default positions for everything held by the defaults but as
; the player moves around the game and interacts with doors/ keys/ items/ etc,
; the buffers keep track of what's been collected, and where the pirates were
; when the player left the room.
;
; When a new game begins, all the rooms are reset to their default states.
@label=UnpackAllRooms
c$AA97 LD HL,$BAAB   ; {Store the starting room table data reference (starting
 $AA9A LD ($BAA5),HL ; at 21 to *TempTableRoomDataPointer.}

; The idea here is to point to the room data, store this in the table.
; Then populate this current room, and then we know what the next address
; value will be for the following rooms starting point.
 $AA9D LD DE,$BCCB   ; Initialise the starting point of the room data for
                     ; populating the room table data - start at room 21.
 $AAA0 LD HL,$ABD6   ; Initialise the default room data (DefaultRoomData) starting
                     ; pointer in HL.

; This part of the loop specifically deals with populating TableRoomData.
@label=UnpackAllRooms_Loop
*$AAA3 PUSH HL       ; Stash the default room data pointer on the stack.
 $AAA4 LD HL,($BAA5) ; {Write the address of where the currently in-focus room
 $AAA7 LD (HL),E     ; table data begins to the room table.
 $AAA8 INC HL        ;
 $AAA9 LD (HL),D     ; }
 $AAAA INC HL        ; {Store the position of the next table entry to *TempTableRoomDataPointer.
 $AAAB LD ($BAA5),HL ; }
 $AAAE POP HL        ; Restore the default room data pointer from the stack.

; Have we finished with everything?
 $AAAF LD A,(HL)     ; {If the terminator character ($FF) has been reached
 $AAB0 CP $FF        ; jump to SetRealStartingRoomID.
 $AAB2 JP Z,$AAEE    ; }

; Now move onto actually copying the room data.
;
; Set up counters for copying data from the default state to the room buffer.
; The counter length relates to the length of the data for each instance of the
; "thing" being copied (NOT the length of the data being copied). For an example;
; portholes are 3 bytes of data each, so B is 3 when calling CopyRoomData.
; How many portholes being copied just depends on when the loop
; reads a termination character ($FF).
 $AAB5 LD B,$01      ; {Handle copying the room colour scheme.
 $AAB7 CALL $ABC2    ; }
 $AABA LD B,$03      ; {Handle copying the scaffolding data.
 $AABC CALL $ABC2    ; }
 $AABF LD B,$04      ; {Handle copying the doors data.
 $AAC1 CALL $ABC2    ; }
 $AAC4 LD B,$02      ; {Handle copying the ladders data.
 $AAC6 CALL $ABC2    ; }
 $AAC9 LD B,$06      ; {Handle copying the keys and locked doors data.
 $AACB CALL $ABC2    ; }
 $AACE LD B,$03      ; {Handle copying the porthole data.
 $AAD0 CALL $ABC2    ; }
 $AAD3 LD B,$10      ; {Handle copying the pirate data.
 $AAD5 CALL $ABC2    ; }
 $AAD8 LD B,$07      ; {Handle copying the items data.
 $AADA CALL $ABC2    ; }
 $AADD LD B,$04      ; {Handle copying the furniture data.
 $AADF CALL $ABC2    ; }
 $AAE2 LD B,$10      ; {Handle copying the lifts data.
 $AAE4 CALL $ABC2    ; }
 $AAE7 LD B,$06      ; {Handle copying the disappearing floors data.
 $AAE9 CALL $ABC2    ; }
 $AAEC JR $AAA3      ; Loop back around to UnpackAllRooms_Loop, the unpacking is only finished when
                     ; the terminator character is read at the start.

; The room ID of $00 just routed the code to UnpackAllRooms; there is no room
; $00 - so set the "real" starting room ID.
@label=SetRealStartingRoomID
*$AAEE LD A,$01      ; {Write $01 to *CurrentRoom.
 $AAF0 LD ($5BD3),A  ; }
 $AAF3 RET           ; Return.

Subroutine: Unpack Room

This subroutine is a lot simpler and hopefully the similarities are clear.

; Unpack Room
;
; This is similar to UnpackAllRooms, however instead of copying ALL the room
; data from the default room data into the room data buffers, this routine
; copies a single room from the room data buffers into the active room buffer.
; The reason for this is that the game opens with the default positions for
; everything held by the defaults but as the player moves around the game and
; interacts with doors/ keys/ items/ etc, the buffers keep track of what's been
; collected, and where the pirates were when the player left the room so when
; they're revisited, they can then retain those changes.
;
; In TableRoomData the pointers to the room data are stored backwards from
; 21-01.
@label=UnpackRoom
c$AAF4 LD A,($5BD4)  ; {Take 21-*TempCurrentRoomID then multiply by 2 (as it's an address we fetch,
 $AAF7 LD E,A        ; so is 16 bit) finally add TableRoomData to point to the correct room buffer
 $AAF8 LD A,$16      ; data address in the room data table for the current room and store the
 $AAFA SUB E         ; pointer in HL.
 $AAFB LD E,A        ;
 $AAFC SLA E         ;
 $AAFE LD D,$00      ;
 $AB00 LD HL,$BAA9   ;
 $AB03 ADD HL,DE     ; }
 $AB04 LD E,(HL)     ; {Fetch the room buffer data address for the requested
 $AB05 INC HL        ; room and store it in DE.
 $AB06 LD D,(HL)     ; }
 $AB07 INC HL        ; Does nothing, HL is overwritten immediately below.

; Move the room buffer data address pointer to the room data itself (the
; first 8 bytes are colour data). There's no need to copy the colours again,
; as they don't vary between each game.
 $AB08 LD HL,$0008   ; {DE+=$0008 (using the stack).
 $AB0B ADD HL,DE     ;
 $AB0C PUSH HL       ;
 $AB0D POP DE        ; }

; Now move onto actually copying the room data.
 $AB0E LD HL,$BAD7   ; HL=BufferCurrentRoomData.

; Set up counters for copying data from the default state to the room buffer.
;
; The counter length relates to the length of the data for each instance of the
; "thing" being copied (NOT the length of the data being copied). For an example;
; portholes are 3 bytes of data each, so B is 3 when calling
; CopyRoomData. How many portholes being copied just depends on when the loop
; reads a termination character ($FF).
 $AB11 LD B,$03      ; {Handle copying the scaffolding data.
 $AB13 CALL $ABC2    ; }
 $AB16 LD B,$04      ; {Handle copying the doors data.
 $AB18 CALL $ABC2    ; }
 $AB1B LD B,$02      ; {Handle copying the ladders data.
 $AB1D CALL $ABC2    ; }
 $AB20 LD B,$06      ; {Handle copying the keys and locked doors data.
 $AB22 CALL $ABC2    ; }
 $AB25 LD B,$03      ; {Handle copying the porthole data.
 $AB27 CALL $ABC2    ; }
 $AB2A LD B,$10      ; {Handle copying the pirate data.
 $AB2C CALL $ABC2    ; }
 $AB2F LD B,$07      ; {Handle copying the items data.
 $AB31 CALL $ABC2    ; }
 $AB34 LD B,$04      ; {Handle copying the furniture data.
 $AB36 CALL $ABC2    ; }
 $AB39 LD B,$10      ; {Handle copying the lifts data.
 $AB3B CALL $ABC2    ; }
 $AB3E LD B,$06      ; {Handle copying the disappearing floors data.
 $AB40 CALL $ABC2    ; }
 $AB43 RET           ; Return.

Routine: Copy Room Data

For completeness; this is the routine which copies the room data.

It’s not that complex, so I won’t over-explain it - all it does is copy x number of bytes from source to destination and checks to see if a termination character has been found in order to end the routine.

; Copy Room Data
;
; B Length of data to be copied
; DE The room buffer target destination
; HL Pointer to the room data we want to copy
;
; This routine copies the number of bytes given by B, from *HL to
; *DE, and keeps on looping until a termination character is returned.
@label=CopyRoomData
c$ABC2 PUSH BC       ; Stash the length counter on the stack.
 $ABC3 LD A,(HL)     ; Fetch a byte from the source room data pointer and store
                     ; it in A.

; Have we finished with everything?
 $ABC4 CP $FF        ; {If the terminator character ($FF) has been reached
 $ABC6 JR Z,$ABD1    ; jump to CopyRoomData_Next.}

; Handle copying the data from the source room data to the target room buffer.
@label=CopyRoomData_Loop
*$ABC8 LD (DE),A     ; Write the room data byte to the room buffer target
                     ; destination.
 $ABC9 INC HL        ; Increment the source room data pointer by one.
 $ABCA INC DE        ; Increment the room buffer target destination by one.
 $ABCB LD A,(HL)     ; Fetch a byte from the source room data pointer and store
                     ; it in A.
 $ABCC DJNZ $ABC8    ; Decrease the length counter by one and loop back to CopyRoomData_Loop
                     ; until the length counter is zero.

; Refresh the same counter as on entry to the routine and start the process again.
 $ABCE POP BC        ; Restore the original length counter from the stack.
 $ABCF JR $ABC2      ; Jump to CopyRoomData.

; This cycle is now over, so store the terminator in the room buffer,
; increment both pointers ready for the next call to this routine and finally, tidy up the stack.
@label=CopyRoomData_Next
*$ABD1 LD (DE),A     ; Write the termination character to the room buffer
                     ; target destination.
 $ABD2 INC DE        ; Increment the room buffer target destination by one.
 $ABD3 INC HL        ; Increment the source room data pointer by one.
 $ABD4 POP BC        ; Housekeeping; discard the length counter from the stack.
 $ABD5 RET           ; Return.

Routine: Populate Current Room Buffers And References

Again for completeness; this is the routine which populates the room buffers and the various reference pointers. I’ve included it simply as it’s referenced in the controller, but it is fairly hardcoded so easy enough to follow:

; Populate Current Room Buffers And References
@label=PopulateCurrentRoomBuffersAndReferences
c$AB44 LD A,($5BD3)  ; A=*CurrentRoom.
 $AB47 LD ($5BD4),A  ; Write A to *TempCurrentRoomID.
; Fetch the room data pointer from the room reference table.
 $AB4A LD E,A        ; {HL=TableRoomData+(($16-A)*$02).
 $AB4B LD A,$16      ;
 $AB4D SUB E         ;
 $AB4E LD E,A        ;
 $AB4F SLA E         ;
 $AB51 LD D,$00      ;
 $AB53 LD HL,$BAA9   ;
 $AB56 ADD HL,DE     ; }
 $AB57 LD E,(HL)     ; {Store the room data address for the requested room in
 $AB58 INC HL        ; HL.
 $AB59 LD D,(HL)     ;
 $AB5A EX DE,HL      ; }

; Set the colour scheme for the active room.
 $AB5B LD DE,$5BCC   ; {Copy $0007 bytes of room data from the
 $AB5E LD BC,$0007   ; buffer to *ActiveRoom_KeyColour.
 $AB61 LDIR          ; }
 $AB63 INC HL        ; Skip the terminator character in the room data.

; Handle populating the scaffolding data.
 $AB64 LD DE,$BAD7   ; {Write BufferCurrentRoomData to *PointerCurrentRoomBuffer.
 $AB67 LD ($5BE8),DE ; }
 $AB6B LD B,$03      ; B=$03 (length counter).
 $AB6D CALL $ABC2    ; Call CopyRoomData.

; Handle populating the doors data.
 $AB70 LD B,$04      ; B=$04 (length counter).
 $AB72 LD ($5BD6),DE ; Write DE to *ReferenceDoors.
 $AB76 CALL $ABC2    ; Call CopyRoomData.

; Handle populating the ladders data.
 $AB79 LD B,$02      ; B=$02 (length counter).
 $AB7B LD ($5BD8),DE ; Write DE to *ReferenceLadders.
 $AB7F CALL $ABC2    ; Call CopyRoomData.

; Handle populating the keys and locked doors data.
 $AB82 LD B,$06      ; B=$06 (length counter).
 $AB84 LD ($5BDA),DE ; Write DE to *ReferenceKeysAndLockedDoors.
 $AB88 CALL $ABC2    ; Call CopyRoomData.

; Handle populating the porthole data.
 $AB8B LD B,$03      ; B=$03 (length counter).
 $AB8D LD ($5BDC),DE ; Write DE to *ReferencePortHole.
 $AB91 CALL $ABC2    ; Call CopyRoomData.

; Handle populating the pirate data.
 $AB94 LD B,$10      ; B=$10 (length counter).
 $AB96 LD ($5BDE),DE ; Write DE to *ReferencePirate.
 $AB9A CALL $ABC2    ; Call CopyRoomData.

; Handle populating the items data.
 $AB9D LD B,$07      ; B=$07 (length counter).
 $AB9F LD ($5BE0),DE ; Write DE to *ReferenceItems.
 $ABA3 CALL $ABC2    ; Call CopyRoomData.

; Handle populating the furniture data.
 $ABA6 LD B,$04      ; B=$04 (length counter).
 $ABA8 LD ($5BE2),DE ; Write DE to *ReferenceFurniture.
 $ABAC CALL $ABC2    ; Call CopyRoomData.

; Handle populating the lifts data.
 $ABAF LD B,$10      ; B=$10 (length counter).
 $ABB1 LD ($5BE4),DE ; Write DE to *ReferenceLifts.
 $ABB5 CALL $ABC2    ; Call CopyRoomData.

; Handle populating the disappearing floors data.
 $ABB8 LD B,$06      ; B=$06 (length counter).
 $ABBA LD ($5BE6),DE ; Write DE to *ReferenceDisappearingFloors.
 $ABBE CALL $ABC2    ; Call CopyRoomData.
 $ABC1 RET           ; Return.

Table: Room Data

This all results in the room data table being nicely populated with pointers! Take a look at the finished table:

; Table: Room Data
;
; Note that room ID 22 is never used, and hence is $0000.
; Room ID 21 is a valid reference (and does have data), but also is not used in the game.
@label=TableRoomData
g$BAA9 DEFW $0000    ; Room 22.
 $BAAB DEFW $BCCB    ; Room 21.
 $BAAD DEFW $BCFE    ; Room 20.
 $BAAF DEFW $BDBE    ; Room 19.
 $BAB1 DEFW $BE92    ; Room 18.
 $BAB3 DEFW $BF66    ; Room 17.
 $BAB5 DEFW $C05D    ; Room 16.
 $BAB7 DEFW $C12D    ; Room 15.
 $BAB9 DEFW $C204    ; Room 14.
 $BABB DEFW $C2BD    ; Room 13.
 $BABD DEFW $C377    ; Room 12.
 $BABF DEFW $C449    ; Room 11.
 $BAC1 DEFW $C4F4    ; Room 10.
 $BAC3 DEFW $C5A2    ; Room 09.
 $BAC5 DEFW $C631    ; Room 08.
 $BAC7 DEFW $C6D6    ; Room 07.
 $BAC9 DEFW $C782    ; Room 06.
 $BACB DEFW $C80C    ; Room 05.
 $BACD DEFW $C8AA    ; Room 04.
 $BACF DEFW $C971    ; Room 03.
 $BAD1 DEFW $CA13    ; Room 02.
 $BAD3 DEFW $CAD9    ; Room 01.

Booty Disassembly