Object Location Data Structure - HoraceAndTheSpider/Bloodwych-68k GitHub Wiki
The data for the on-map location of objects is stored for each tower/dungeon immediately after its map data, in a 1026-byte ($402) block: two bytes to say how much is used, followed by 1024 bytes ($400) of space for the objects. In the PC version this is contained in the tower's .ob file. The block begins with a two-byte count of the bytes used by the placement records.
Data Locations: (Amiga - SPS 102)
These 1026 byte blocks are found at the following locations:
[$0FC7C] The Keep [$1107E] The Serpent Tower [$12480] The Moon Tower [$13882] The Dragon Tower [$14C84] The Chaos Tower [$16086] Zendik's Tower
The data for the objects is stored in such a way that there is no fixed length for each entry within the data table. This is to allow for only one entry when items are placed on top of one another in the same location.
The first two bytes of data represent the amount of data used for item-placements listed within the data table, and are then followed by the data itself. This count does not include the two count bytes.
In the SPS 439 version, in the case of the Keep [$FBBC] (PC: mod0.ob) this is a value of 03 3F.
This is then followed by a minimum of 5 bytes, with an additional 2 bytes added for each extra item in this location.
This can be represented as the following;
AB CD EF GH IJ ... KL MN ... etc
For the standard map sizes, A shows the position of the items in the location (four positions for a standard floor, and two accessible levels for a shelf)
This has one of four options;
$0 - North-west floor corner; north-facing lower shelf; west-facing upper shelf
$4 - North-east floor corner; east-facing lower shelf; north-facing upper shelf
$8 - South-west floor corner; west-facing lower shelf; south-facing upper shelf
$C (decimal 12) - South-east floor corner; south-facing lower shelf; east-facing upper shelf
The shelf meanings are deliberately orientation-dependent. A record describes a stored corner/mini-space, not an absolute screen side. A shelf has two levels, but their pair of stored mini-spaces changes with the face on which the shelf is mounted.
BCD = The index number for the location in a standard-sized map.
The full format also allows the two lower bits of A to be part of the location number. These are zero for the original map sizes, so the examples below can use the familiar A / BCD split. The exact bit layout is given in the source details.
This is a map-data byte offset, rather than an independent x/y/z coordinate. The SPS 439 `CoordToMap` path calculates it from the tower floor, row and column and addresses the two-byte map cell at that offset. It is consequently equivalent to counting map cells in map order and multiplying by two: `$0000` is the first cell on the lowest floor, and the following cell is `$0002`.
EF = The number of items in the given location, minus 1.
Where there is only one item stored in the location, this has a value of 00. Where there are two items, a value of 01 and so on. This is an important piece of information as with no "termination" byte, it is this which is used to tell us how many bytes of data will need to be read for the object data for this location.
GH - This is the object code for the first item in the location. This is a representation of the item, based on the object lookup table, which will need to be referenced in order to determine which items are in the location.
IJ - This is the number of item GH to be "stacked" at this point. Whilst this is commonly used for Coinage and Common Keys, which are usually place in multiple numbers, this can also be used to stack multiple numbers of any item, to a maximum number of 255.
KL - This is identical to GH, in those situations where EF is greater than zero. It gives the object code for the next item stored at a given location.
MN - Again, this is identical to IJ, in those situations where EF is greater than zero, and gives the number of the item represented bu KL to be stored at the location.
In order to aid the understanding of the object data structure, here is an example piece of code, from the first item stored for the Keep.
C8 0E 00 07 01
This data would represent: 1 item on the location (00 on EF.) 1 (01 on IJ) x Apple (07 on GH) at index postion 8 0E (8 OE on B CD), in the C position (C on A.)
The next set of data in the list is:
88 0E 00 07 01
This data would represent; 1 item on the location. 1 x Apple at index position 8 0E, in the 8 position of the location.
And skipping through to the fourth piece of data we see the following;
88 8E 02 50 01 30 01 02 05
Which represents; 3 items on the location 1 x Bronze Key , 1 x Dagger, 5 x Common Keys at index position 8 8E in the 8 position of the location.
Further source details (SPS 439)
The descriptions and worked examples above explain how to read a record. These notes give the exact sizes and show how the game finds and changes those records.
SPS 439 resource locations
These are original-executable file offsets, not runtime addresses.
| Tower | Original source label | File offset | Extracted resource |
|---|---|---|---|
| The Keep | ObjectData_1 |
$FBBC |
maps/mod0.obj |
| Serpent Tower | ObjectData_2 |
$10FBE |
maps/serp.obj |
| Moon Tower | ObjectData_3 |
$123C0 |
maps/moon.obj |
| Dragon Tower | ObjectData_4 |
$137C2 |
maps/drag.obj |
| Chaos Tower | ObjectData_5 |
$14BC4 |
maps/chaos.obj |
| Zendik's Tower | ObjectData_6 |
$15FC6 |
maps/zendik.obj |
The first word is the big-endian number of used payload bytes, excluding the length word itself. The Keep starts with $033F. Unused capacity follows the records; it is not another record or a terminator.
Variable-length records
A record holds all object entries at one map cell and one mini-space:
| Record offset | Size | Meaning |
|---|---|---|
| +0 | 2 bytes | Packed mini-space and map-cell byte offset |
| +2 | 1 byte | Number of object/quantity pairs minus one |
| +3 | 2 bytes per pair | Object code, then quantity |
For count-minus-one n, there are n + 1 pairs and the record occupies 5 + 2 * n bytes. Successive records are packed with no alignment padding. A length-aware parser must reject records that run beyond the used payload or the resource allocation.
Packed location
The location word is:
location = (mini_space << 14) | map_byte_offset
mini_space = location >> 14
map_byte_offset = location & $3FFF
The offset is 14 bits, not 12. The four familiar high-nibble values $0/$4/$8/$C arise when the stock map's offsets fit in the low 12 bits; the low two bits of that high nibble still belong to the offset.
The map offset addresses a byte in the cell payload after the $38-byte map header. It is neither a standalone cell index nor an X/Y coordinate:
map_byte_offset = floor_data_offset + 2 * (y * floor_width + x)
Valid map-cell offsets are even. The 14-bit encoding does not by itself permit larger maps: the existing resource size and other engine bounds still apply.
Mini-spaces
| Mini-space | Packed contribution | Floor position |
|---|---|---|
| 0 | $0000 |
North-west |
| 1 | $4000 |
North-east |
| 2 | $8000 |
South-west |
| 3 | $C000 |
South-east |
Shelf positions are orientation-dependent. Stored mini-space 0 is a north-facing lower shelf or west-facing upper shelf; 1 is an east-facing lower shelf or north-facing upper shelf; 2 is a west-facing lower shelf or south-facing upper shelf; 3 is a south-facing lower shelf or east-facing upper shelf. A record describes a stored mini-space, not an absolute screen side.
For example, C8 0E 00 07 01 describes mini-space 3, map byte offset $080E, one pair, object code $07, quantity 1. The location is $080E, not $C80E.
Runtime scanning and insertion
Original adrCd000960 scans the used records and sets second-byte bit 6 in their map cells. These are object-presence markers, not trap markers. adrCd005F5C searches for a stack by comparing the packed location, advancing each record by 5 + 2 * count_minus_one.
Both paths locate the object payload from the current map-cell base using $0FCA:
object records = cell-data base + ($1000 - $38 + 2)
length word = object records - 2
The insertion path at adrCd005EAC grows the used length by two for another object/quantity pair. The new-stack path at adrCd005F04 grows it by five. Neither path compares the new length against a $400 or $402 allocation limit before writing. Without a separate capacity check, an insertion beyond the reserved space can overwrite the data that follows it.
There is no total-object-allocation operand in these scan/append paths that could simply be set to “the longest object file.” The stock $402 size is an allocation boundary, while the routines consume the resource's used-length word.
The 16-byte table at original adrB_005F3E converts a player-relative drop corner into a stored mini-space. Its four facing rows are 0,1,2,3; 1,3,0,2; 3,2,1,0; and 2,0,3,1. The lookup index is facing * 4 + relative_corner. This is placement logic, separate from first-person object projection.
Enlarged resources
Changing object allocation length shifts later tower resources. The assembled map-offset differences account for those shifts only while their signed-word range remains valid. The relationship between each map and its following object header must also remain correct.
Increasing this space requires matching changes in the compiled source, its addresses and its save layout. Simply making an object file longer does not change where the game expects to find the next block, or prove that extra object insertions are safe.
See Map Data Structure for the map allocation and relocation constraints. The structures and routines above are verified against the original SPS 439 disassembly and its original map/object bytes.
SPS 439 renderer verification
The first-person renderer scans all nineteen player-relative view cells. This includes view cell 18, the player's standing square. Draw_DungeonCellFloorObjects then walks the four stored mini-spaces and invokes Draw_ObjectOnFloor for every object record linked to a visible mini-space. The current square is therefore not a special “no objects” case.
Floor-object placement is source-table driven. GFX_ObjectsOnFloor_SubpositionRotation converts the stored $0/$4/$8/$C mini-space to the player-relative one; the X-position, special X-position, base Y and shape-Y-adjustment tables then select the final projection. $80 in an X-position table is the explicit hidden-position sentinel. Wide object shapes use a separate graphics bank and width selector.
Shelves are drawn by the main-wall overlay path, not as free-standing floor objects. Draw_Main_Shelf_Overlay checks the selected wall face's visibility before Draw_Main_Shelf_Visible selects and places shelf records. Consequently a shelf's contents belong to its face-on wall feature and must not be shown from a view where that face is absent or occluded.
The companion data has the following logical boundaries in SPS 439:
| Source label | Purpose | Resource name used here |
|---|---|---|
adrB_009682 |
Combined mini-space rotation, depth and Y-adjustment layout | gfx-data/ObjectsOnFloor_Projection.layout |
adrB_0097B6 |
Projection widths | gfx-data/ObjectsOnFloor_Widths.widths |
adrEA0097BC |
Ordinary projected X positions | gfx-data/ObjectsOnFloor_XPositions.positions |
adrEA009808 |
Shelf/special projected X positions | gfx-data/ObjectsOnFloor_SpecialXPositions.positions |
adrEA00981C |
Object-shape Y adjustments | gfx-data/ObjectsOnFloor_YAdjustments.positions |