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:
- 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). - The next two bytes become live bytes
$00and$01. If the X byte has its top bit set, the initial map-cell occupancy mark is skipped. Team followers normally use$FFfor this no-position marker. - The level byte is copied to live
$06and$07. Live$03is derived from the level, and the word at live$08–$09is calculated using a level-dependent multiplier before adding$19. - The form byte becomes live
$0B. In the original game data, form$40is reserved for Zendik and occurs once for this purpose. It causes live$0Cto receive$37, the existingObject_AceOfSwordsobject code. Other$40checks 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. - 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:
KL = $FFmeans there is no team-table entry.- Otherwise, the game writes this monster's record number into slot
table[KL]. This happens for both the leader and the followers. - If this monster has a positioned X coordinate, it also receives
KL >> 2in live byte$0D, and the team-count word is incremented. - An unpositioned follower keeps
$FFin 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:
adrCd0009F6andadrLp000A08: initialise the team-count word and fill the 25 team rows with$FF.adrCd000AEC(existing relabelStoreTeamData): store member record numbers, and assign a group number only to the positioned leader.adrCd009A2A(existing relabelDraw_MonsterTeamOccupants) andadrLp009A46: resolve and draw the four team slots.adrCd002848,adrCd00287CandadrCd0028BC: 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,adrLp0079B4andadrLp0079EA: 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
$06and base level$07after status effects; - the meaning of live bytes
$0E–$0F.