Monster Data Structure (Live) - HoraceAndTheSpider/Bloodwych-68k GitHub Wiki

Monster Data Structure (Live)

At the start of each dungeon/tower, the game expands the tower's packed monster data described on Monster Data Structure into a live workspace. This is the form used by movement, combat, AI, trading, and rendering routines.

Location and capacity

The addresses here are runtime memory addresses. For SPS 439, live records begin at $16B7E (UnpackedMonsters). A word immediately before the workspace, at $16B7C, stores the last live-record index: number of records minus one, or $FFFF for none. The workspace is filled with $FF as 512 longwords ($800 bytes), so it can hold 128 records of 16 bytes each. The per-tower counts in the packed data remain the authoritative number of records to unpack; 128 is the allocated workspace capacity, not a claim that every tower contains that many monsters.

The corresponding BEXT live base is $14EE6. The offsets below are common to the SPS 439 unpacking code; variant-specific addresses should be verified separately. The count word is a DBRA terminal index: a non-negative value represents that value plus one records, while $FFFF represents none.

Record layout

Each record begins 16 bytes after the preceding record. A raw first record may look like:

0D 12 12 E9 03 00 00 00 00 19 00 69 FF FF FF FF
Offset Name Meaning
$00 X position Starting or current X coordinate.
$01 Y position Starting or current Y coordinate.
$02 Rotation/space state Runtime orientation and/or occupied-space state; cleared during unpacking.
$03 Action countdown Level-derived initial countdown, subsequently used by runtime behavior.
$04 Floor Zero-based floor value after unpacking.
$05 Action/status state Cleared during unpacking and used later for runtime flags/state. Individual bits are not fully documented yet.
$06 Current level Initially copied from the packed level; later code can use this as the current/status-adjusted level.
$07 Base level The packed monster level.
$08–$09 Hit points Starting/current hit points as a word. The unpacker calculates the initial value from the level and its multiplier.
$0A Monster type Type nibble from the packed record.
$0B Monster form Form/look index from the packed record.
$0C Carried/traded object Object code used by monster trading/drop behavior. In the original SPS 439 data, form $40 is reserved for Zendik and is assigned Object_AceOfSwords ($37) here.
$0D Leader's team group Group number on the positioned leader; $FF on solo monsters and unpositioned followers after unpacking. Followers are found through the separate Teams table.
$0E–$0F Unresolved/reserved Not populated by the tower unpacker; their later uses still need definition.

The original notes record seeing $85 in byte $06 during paralysis. That observation is retained here, but the precise status encoding is still unresolved; it should not be read as a confirmed level of 133.

Values shown as zero in a sample dump are therefore not all permanent meanings. Some fields are deliberately initialized and acquire runtime state later.

Attack types and the live record

The attack routines keep a pointer to the current 16-byte live monster record in address register A4. References such as $0004(a4) are therefore fields in the record above, not a separate AI structure. In the attack-type dispatcher the important offsets are:

A4 offset Field Use in this path
$00 X position Packed into the launch coordinate and used by pursuit distance calculations.
$01 Y position Packed into the launch coordinate and used by pursuit distance calculations.
$02 Rotation/space state Low two bits supply the monster or projectile facing.
$04 Floor Passed to a new projectile and compared with each fixed player-origin floor.
$05 Action/status state Set to $1F when a monster launches a spell.
$06 Current level Used by movement and special-form behaviour.
$07 Base level Seeds ordinary monster spell power and biases spellbook selection.
$0A Monster type Selects one of the five attack behaviours.
$0B Monster form Distinguishes ordinary and special live entities.
$0C Carried object/caster context Copied into shared object and live-entity handling where required.
$0D Team group Routes attacks involving grouped monsters.

MonsterAttackTypeTable contains five signed branch displacements, indexed by live byte $0A:

Type Behaviour
$00 No spells: pursuit, movement and melee.
$01 Normal spellcasting, otherwise the no-spells path.
$02 Drone movement without normal spell selection.
$03 Drone movement with a one-in-sixteen spell-selection chance.
$04 Fixed Arc Bolt machine attack at original adrJA001664.

The spellbook selection and power calculation are detailed on Monster Attack Spells.

How the unpacking works

The unpacking routine reads one six-byte packed record at a time:

  1. The first packed byte is decremented, then split into two nibbles. The high nibble becomes live byte $0A (monster type); the low nibble becomes live byte $04 (zero-based floor).
  2. The next two bytes become live bytes $00 and $01. If the X byte has its top bit set, the initial map-cell occupancy mark is skipped. Team followers normally use $FF for this no-position marker.
  3. The level byte is copied to live $06 and $07. Live $03 is derived from the level, and the word at live $08–$09 is calculated using a level-dependent multiplier before adding $19.
  4. The form byte becomes live $0B. In the original game data, form $40 is reserved for Zendik and occurs once for this purpose. It causes live $0C to receive $37, the existing Object_AceOfSwords object code. Other $40 checks provide Zendik-specific behavior, including his identity response and special standing/centre behavior. The form could technically be reused by another monster, but the original programmers avoided doing so.
  5. The final packed byte is used to build the team index table described below.

When a tower is left, surviving live records are packed back into the tower data. Re-entering a tower can therefore reconstruct some values and reset runtime-only statistics such as hit points.

The separate Teams table

A team's members still have their own live monster records: their own form, level, hit points and other state. The Teams table does not replace those records. It is a small list telling the game which records belong together.

Each team has four slots. A slot holds a zero-based live monster-record number, or $FF if no member occupies it. It does not hold a monster form, a memory pointer, or the packed KL value.

Layout in memory

Runtime address Size Meaning
$1738E 2 bytes Last team-row index: number of teams minus one; $FFFF means none.
$17390 4 bytes Team 0: one record number for each of slots 0–3.
$17394 4 bytes Team 1: slots 0–3.
$17398 4 bytes Team 2: slots 0–3.
… … Further team rows, four bytes apart.
$173F0 4 bytes Team 24: the last row in the allocation.

The table occupies $64 bytes (100 bytes), ending immediately before $173F4. Its original label is adrEA017390, relabelled MonsterTeamIndexTable. The preceding word is original adrW_01738E, relabelled MonsterTeamGroupCount; despite that name, its stored value is a last index, not an ordinary count.

For group g, slot s and record number r:

team-slot address   = MonsterTeamIndexTable + 4*g + s
live-record address = UnpackedMonsters + 16*r

A separate count at $16B7C describes all live monster records. It is not the team count: two teams with two members each account for four monsters, but only two team rows.

How packed data builds this table

At tower entry, all 100 slot bytes are set to $FF, and the team-count word starts at $FFFF. The game then reads the tower's six-byte monster records in order.

For each record:

  1. KL = $FF means there is no team-table entry.
  2. Otherwise, the game writes this monster's record number into slot table[KL]. This happens for both the leader and the followers.
  3. If this monster has a positioned X coordinate, it also receives KL >> 2 in live byte $0D, and the team-count word is incremented.
  4. An unpositioned follower keeps $FF in live $0D; its membership is held in the table, not copied into that field.

For four positioned team leaders, the count therefore changes $FFFF → $0000 → $0001 → $0002 → $0003. The final $0003 tells the team-processing loops to visit rows 0 through 3.

The original data uses one positioned leader per team, distinct slots, and consecutive group numbers starting at zero. Duplicate KL values write to the same slot, so the later record replaces the earlier record number there. The unpacker does not validate or automatically repair those entries.

Example: The Keep's initial Teams table

Reconstructing the table from the original SPS 439 Keep records gives:

Team group Slot 0 Slot 1 Slot 2 Slot 3
0 $0B $0C $FF $FF
1 $12 $13 $FF $FF
2 $3A $3B $FF $FF
3 $3D $3E $3F $FF

The remaining rows are all FF FF FF FF; the count word is $0003. These four teams contain nine monsters altogether. The Keep's other 64 starting records are not members of those teams.

For team 0, live record $0B holds the location and has $00 in byte $0D. Record $0C has X = $FF and $FF in byte $0D, but it is still a member: slot 1 of team 0 points to it.

The packed-record example shows the six-byte records which produce the first two rows.

How the game uses it

When drawing a dungeon square, the game finds the positioned monster first. If that monster's live $0D contains a group number, the renderer looks up the team's four slots and draws their members at that shared map position. Facing and slot order determine the formation; a follower's separate Y byte does not supply another drawing location.

The table can change during play. When a monster record is removed, team references are adjusted to follow the remaining records. If the removed monster held the team's position, another member receives its X, Y, floor, facing and group number. The team-cleanup routine also removes empty groups and dissolves a group with only one remaining member.

On leaving a tower, the packing routine uses the current table to rebuild the members' KL bytes as group * 4 + slot. The table is runtime workspace, not a second independently authored list of starting monsters. The packed records are sufficient to rebuild it at the next tower entry.

Source references (SPS 439)

The relevant parts of the original source are:

  • adrCd0009F6 and adrLp000A08: initialise the team-count word and fill the 25 team rows with $FF.
  • adrCd000AEC (existing relabel StoreTeamData): store member record numbers, and assign a group number only to the positioned leader.
  • adrCd009A2A (existing relabel Draw_MonsterTeamOccupants) and adrLp009A46: resolve and draw the four team slots.
  • adrCd002848, adrCd00287C and adrCd0028BC: update table entries after record removal and transfer a removed leader's position.
  • adrCd001090: compact the team list and remove groups which no longer need it.
  • adrCd007974, adrLp0079B4 and adrLp0079EA: pack live monsters and reconstruct their team bytes.

Remaining uncertainties

The source behavior is clear for the fields populated during unpacking, but the following meanings should not yet be treated as fully settled:

  • the individual bits of live byte $05;
  • the exact distinction between current level $06 and base level $07 after status effects;
  • the meaning of live bytes $0E–$0F.