Monster Data Structure - HoraceAndTheSpider/Bloodwych-68k GitHub Wiki

Monster Data Structure

In the game binary, monster information is stored in batches by tower in a packed form. At tower entry, this data is expanded into the 16-byte live records described on Monster Data Structure (Live).

Per-tower storage

The addresses in this section are file offsets in the SPS 439 executable. The original source's runtime addresses are $384 higher: for example, MonsterTotalsCounts is at runtime $17578, file offset $171F4.

For SPS 439, six words at file offset $171F4 store the final zero-based monster-record index for each tower. The tower-entry and tower-exit loops use DBRA, so a non-negative word n expands or writes n + 1 records; $FFFF means that the tower has no records.

As raw data, these six words are:

00 48 00 4D 00 53 00 43 00 4F 00 4D

These mean 73, 78, 84, 68, 80 and 78 records respectively—not 72, 77, 83, 67, 79 and 77. Counting starts at record zero.

They are followed by six $300-byte blocks, each containing 128 possible six-byte slots:

$17200  The Keep
$17500  The Serpent Tower
$17800  The Moon Tower
$17B00  The Dragon Tower
$17E00  The Chaos Tower
$18100  Zendik's Tower

$300 is hexadecimal, or 768 decimal bytes. The final block therefore occupies $18100–$183FF in SPS 439. The terminal-index word, rather than the unused tail of a block, determines how many records are expanded for a tower. For example, The Keep's $0048 expands records $00 through $48: 73 records, including the final $66 form at floor 4, X $0B, Y $0A.

Six-byte record

The packed records are six bytes long. The first eight records in The Keep are:

04 0B 0F 00 15 FF
04 16 1A 00 16 FF
05 02 00 04 15 FF
04 0F 0F 00 2F FF
14 03 09 00 11 FF
04 06 00 01 14 FF
04 06 01 00 14 FF
04 06 03 01 14 FF

The bytes can be written as:

AB CD EF GH IJ KL

A — monster type

In Bloodwych, the high nibble identifies the broad monster type:

  • 0 — normal fighting monster
  • 1 — spell-casting monster
  • 2 — normal fighting drone
  • 3 — spell-casting drone
  • 4 — arc-bolt machine

Extended Levels also uses 5 for a standing NPC.

The SPS 439 unpacker decrements the first byte before splitting it. Its high nibble becomes the live record's type byte $0A; its low nibble becomes the live record's zero-based floor byte $04. This explains the apparent one-based floor values in the packed bytes.

B — starting floor

The low nibble of the decremented first byte is the floor on which the monster starts. The packed representation is one-based at this point in the source data; the live representation is zero-based after the decrement.

CD and EF — starting coordinates

CD contains the X-coordinate and EF contains the Y-coordinate. If two to four monsters start as a team at the same location, one member holds the position. The other members have $FF for X: they do not occupy separate map squares.

The unpacker copies these bytes into live $00 and $01. An X byte with its top bit set skips the initial map-cell occupancy mark; $FF is the usual no-position marker. The team byte KL, explained below, links those unpositioned members to their team. $FF in X alone does not mean “join the previous monster.”

GH — monster level

GH is the monster level. It determines strength, speed, available spells, and, for non-humanoid monsters, the colour grade. In Extended Levels it also acts as a seed used when generating the monster's name.

The unpacker copies this value to both live bytes $06 and $07. It derives the initial action countdown and hit points from the level and monster type. The live record can subsequently distinguish a current/status-adjusted level from the original base level.

IJ — monster form

IJ selects the monster's form/look from the monster lookup data. The unpacker stores it in live byte $0B.

One special form is significant: in the original SPS 439 data, form $40 is reserved for Zendik and occurs once. The unpacker puts object code $37 (Object_AceOfSwords) into live byte $0C. Other $40 checks provide Zendik-specific behavior, including his identity response and special standing/centre behavior. The packed record itself does not contain the object code directly. Although the form could technically be reused, the original programmers avoided doing so.

KL — team data

The team byte tells the game which team this monster belongs to, and which of that team's four places it occupies. Most records contain $FF, meaning no team-table entry.

Think of each team as a row with four spaces. The first team uses values $00–$03, the next uses $04–$07, and so on:

Team group Slot 0 Slot 1 Slot 2 Slot 3
0 $00 $01 $02 $03
1 $04 $05 $06 $07
2 $08 $09 $0A $0B
3 $0C $0D $0E $0F

For example, two monsters teamed at X04 Y06 can use $00 and $01. A different pair uses $04 and $05, not $02 and $03. Values $02 and $03 would fill the other two places in the first team.

The exact calculation is:

KL          = team_group * 4 + member_slot
team_group  = KL >> 2
member_slot = KL & $03

These slot numbers are positions within the formation, not monster types or form numbers. The renderer uses the team's facing and the player's viewing direction to work out where each member appears on screen.

What makes a complete team?

A team needs both its placement information and its KL entries:

  • One positioned member acts as the leader: it holds the team's X/Y position and floor.
  • Other members normally have X = $FF. Their shared position is reached through the team, not by giving each one another occupied square.
  • All members use the same group number and different slots in that group.
  • The leader needs a KL entry too. Giving only a follower team data does not connect it to an otherwise solo leader.

The leader is the member which holds the position, not a separate monster type. The original records place it first, followed by the other members. Membership itself is recorded by KL, rather than inferred just from neighbouring records.

The follower's Y byte is still preserved, but it does not have to equal the leader's Y. For example, Serpent Tower's group $0B contains:

14 09 02 04 38 2C
04 FF 0A 03 34 2D
04 FF 12 04 32 2E

The positioned member is at X $09, Y $02, on floor 3. The other two records have different Y bytes, yet $2C, $2D and $2E put them in slots 0, 1 and 2 of that same team. They are drawn using the positioned member's map location.

Example: the first two teams in The Keep

These are actual six-byte records from the original SPS 439 data. Record numbers are zero-based and hexadecimal:

Record number Packed bytes Meaning
$0B 04 15 00 00 29 00 Team 0, slot 0; holds X $15, Y $00, floor 3.
$0C 14 FF 00 01 12 01 Team 0, slot 1; shares that team's position.
$12 14 17 18 02 4B 04 Team 1, slot 0; holds X $17, Y $18, floor 3.
$13 04 FF 18 01 11 05 Team 1, slot 1; shares that team's position.

On entering the tower, the game turns these into the following two rows in a separate runtime Teams table:

Team 0:  0B 0C FF FF
Team 1:  12 13 FF FF

The table contains record numbers, not the KL values. For example, KL = $04 selects slot 0 of team 1; the byte stored there is $12, telling the game to use live monster record $12.

The full runtime layout and the rest of The Keep's example are described under Monster Data Structure (Live) — the separate Teams table.

Team and record limits

SPS 439 reserves 25 four-byte team rows: 100 member slots. Within that allocation, group numbers run from 0 to 24 and KL values from $00 to $63; $FF remains the no-team marker. The unpacker uses KL directly as a table offset without checking that it fits the allocation.

The original data uses consecutive group numbers starting at zero, with one positioned leader per group. This matters because the runtime group-count word is built by counting those leaders, not by taking the largest group number. A gap in group numbering is not automatically repaired during unpacking.

The live workspace reserves 128 monster records, including solo monsters and team members. These are source allocation sizes, not proof that every combination at those limits is safe throughout gameplay.

Runtime expansion

At tower entry the game clears the 25-group team table and the $800-byte live workspace, selects the current tower's $300-byte packed block, and expands the number of records from that tower's count word. Each six-byte packed record therefore becomes one 16-byte live record. The live form contains fields needed during play—calculated hit points, countdown/state, carried object, and team lookup—that are not present as separate fields in the packed six-byte representation.

When the tower is exited, surviving monsters are packed back into the per-tower data. Re-entering a tower consequently reconstructs some live values from the original packed fields and can reset runtime-only statistics.

Further source details (SPS 439)

The original FillMonstersLoop expands each record. Its team-handling part, adrCd000AEC (relabelled StoreTeamData), writes the record number into the Teams table and assigns the group number to the positioned leader. The packing routine at adrCd007974 later uses that table to reconstruct each member's KL byte.

Address note

The original disassembly contains adrL_0186A0 inside the apparent Zendik allocation. Its only apparent reference is move.l #adrL_0186A0,d1 in a disk-read wait loop, where the value is decremented as a timeout counter. The value $000186A0 is therefore a timeout literal that happens to equal a data address, not evidence of another monster-data boundary. It should be represented as a timeout constant in cleaned source.