Previously, I thought a way to uncover what was happening in the code in Wicked was to look at the sound effects and tie the sounds to when they were triggered. I was left looking at where the value of D0 was set, and this led me into discovering the event signalling mechanism in Wicked. The event system uses a dedicated four byte buffer at address 39290, allowing the game to queue up to four sound events at a time. These events directly trigger the sound effects tied to gameplay occurrences.
Event slot addresses:
39290— slot 039292— slot 139294— slot 239296— slot 3
Each byte corresponds to one of the four Amiga audio channels. The event ID placed into a slot determines which sound effect will be played on that channel. When a value other than $FF is written to one of these slots, the sound effect system activates and begins playback on the corresponding channel, based on a fixed lookup table at address 39658.
These slots are not used strictly sequentially. Instead, the subroutine SUB_2E3CA evaluates priority and fills them accordingly, as seen by the three-pass logic in the code (matching, empty, then lower-priority scans).

Priority-Driven Insertion
Each event ID is mapped to a priority using a lookup table at 39276. This table is a byte array indexed by event ID, with lower values indicating higher priority. When a new event is triggered, the code:
- Looks up the priority for the incoming event.
- Scans the event buffer for a matching ID to avoid duplication.
- Searches for an unused slot (value
$FF). - If all slots are occupied, attempts to evict a lower-priority event.
The system ensures that low-priority events don’t overwrite important ones already in the queue. If no suitable slot is found, the new event is discarded and no sound is triggered.
Decoding the Priority Table
The priority table at LAB_PriorityTable_39276 contains 26 bytes, one per event ID from $00 to $19. Each byte specifies the event’s priority. The code in SUB_2E3CA at 2E40A performs a lookup using:
MOVE.B 0(A2,D0.W),D2 ; D0 = event ID, A2 = base of priority table
- Structure:
- The table starts at 39276.
- Interpretation:
- The table is indexed by event ID ($00 to $19 in hex).
- Each byte represents the priority value for the corresponding event ID. Lower values indicate higher priority (e.g., $01 is higher priority than $02 or$03).
- The table is treated as a byte array, with MOVE.B 0(A2,D0.W),D2 at 2E40A using D0 (the event ID) as an offset into A2 (the priority table base).
- Priority Values:
- Extracting the byte values in order (0 to 25 indices):
- 0x01, 0x02, 0x01, 0x01, 0x01, 0x00, 0x01, 0x02, 0x02, 0x01, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01
- Mapping to event IDs ($00 to $19) we get:
- Extracting the byte values in order (0 to 25 indices):
| Address | Priority Data (Hex) | Event ID |
|---|---|---|
| 39276 | 01 | $00 |
| 39277 | 02 | $01 |
| 39278 | 01 | $02 |
| 39279 | 01 | $03 |
| 3927A | 01 | $04 |
| 3927B | 00 (Highest) | $05 |
| 3927C | 01 | ($06) |
| 3927D | 02 | $07 |
| 3927E | 02 | $08 |
| 3927F | 01 | $09 |
| 39280 | 03 (Lowest) | ($0A) |
| 39281 | 01 | $0B |
| 39282 | 01 | $0C |
| 39283 | 01 | $0D |
| 39284 | 01 | $0E |
| 39285 | 01 | $0F |
| 39286 | 01 | $10 |
| 39287 | 01 | $11 |
| 39288 | 02 | $12 |
| 39289 | 02 | $13 |
| 3928A | 01 | $14 |
| 3928B | 01 | $15 |
| 3928C | 01 | $16 |
| 3928D | 01 | $17 |
| 3928E | 01 | $18 |
| 3928F | 01 | $19 |
- The table assumes a continuous 0–25 index range.
- Priority direction – the comparison uses
BHI(“branch if higher” = incoming priority byte larger), so smaller numbers are higher priority. e.g EventID$05has byte $00⇒ can evict anything with byte $01–$03.
Priority Direction
The insertion logic uses BHI (Branch if Higher) when comparing existing vs incoming priorities. Since BHI branches when the existing priority value is higher, this means smaller values are considered higher priority. For example, event ID $05 (Servant being shot) has priority $00 and can displace any event with priority $01 or higher.
All of the insertion / scanning work happens inside SUB_2E3CA.
Key instructions:
| Role | Instruction | What it does |
|---|---|---|
| Duplicate / matching scan | MOVEQ #3,D1 … DBF D1,SearchForMatchingEvent | Walks all four slots (indices 3→0) looking for the same ID. Reject a second copy of the same ID. (CMP.B 0(A1,D1.W),D0 then DBF) |
| Empty-slot scan | reset D1 to 3, test each byte against $FF | Finds the first unused slot, by looking for the first $FF (unused entry). |
| Priority lookup | LEA LAB_39276,A2 / MOVE.B 0(A2,D0.W),D2 | Grabs the incoming event’s priority byte. Compare incoming priority (D2) with the existing slot’s priority (0(A2,D3.W)) |
| Replace-lower-priority | compare D2 with 0(A2,D3.W) per slot | Overwrites an existing lower-priority event if necessary, if the new value is higher |
| Final write | MOVE.B D0,0(A0,D1.W) (buffer at 39290) | Commits the ID to the primary four-word ring. |
SUB_2E3CA uses three consecutive loops to do this:
- Duplicate scan:
CMP.B 0(A1,D1.W),D0
DBF D1,...
Searches all four slots for the same event ID. If found, the event is not inserted again. - Empty slot scan:
CMPI.B #$ff,0(A1,D1.W)
Scans for any$FFentry, marking it as unused. - Lower-priority replacement:
CMP.B 0(A2,D3.W),D2 ; D2 = incoming priority, D3 = existing slot's event ID
BHI ...
If the incoming event has a higher priority, it can evict the existing one.
Each phase loops over D1 from 3 to 0. All slots are treated independently; there is no pointer arithmetic. Matching, emptiness, and priority replacement all happen through direct indexed comparison.
Audio Subsystem Connection
The slots at 39290 are not abstract queues, they directly feed into the Amiga audio system:
- The sound handler
SUB_ProcessActiveSoundEffects_39294iterates over the same 4-byte buffer. - If a slot is not
$FF, it plays the corresponding sound effect usingSUB_392D8. - The event ID is used as an index into a sound descriptor table at
39658. - Playback configuration is written into the Amiga’s
AUDxLCH(DFF0A0),AUDxLEN(DFF0A4), andDMACON(DFF096) registers.
This confirms that each event ID triggers a specific sound effect, and only four can be active simultaneously.
Trigger Points and Event IDs
Event IDs are generated by game logic routines before calling SUB_2E3CA. The D0 register is loaded with the event ID, followed by a direct JSR into the subroutine
| Event ID | Address that sets D0 | Inference/Speculation | Priority |
|---|---|---|---|
| 0x00 | 0x2D9C8 | Central face opens for tarot | 0x01 |
| 0x01 | 0x2B57C | Evil spore converted into a portal | 0x02 |
| 0x02 | 0x2AA92 | Good spore dropped on good growth, converted into a good portal | 0x01 |
| 0x03 | 0x2B2CE | Trigger for power crystal? | 0x01 |
| 0x04 | 0x28B9C | Grave screen initialization is complete following game over | 0x01 |
| 0x05 | 0x2AE00 | Guardian’s servant shot | 0x00 |
| 0x07 | 0x2AB42, 0x2ABEE | Bullet(s) fired – single and burst variants | 0x01 |
| 0x08 | 0x2A85C | Level starts Guardian initial spawn (sets sprite & movement slot) | 0x02 |
| 0x09 | 0x2D9F6 | Day/night change | 0x02 |
| 0x0B | 0x2B124 | Evil spore created | 0x02 |
| 0x0C | 0x2B2EA | Central face closed (FaceAnimationCounter_434 Reaches 658 $292) | 0x01 |
| 0x0D | 0x2BF54 | Energy update – gain / loss | 0x01 |
| 0x0E | See 2E42C | Central face opens, Power Crystal appears (FaceAnimationCounter_434 Reaches 544) | 0x01 |
| 0x0F | 0x2BE30 | Power crystal collected | 0x01 |
| 0x10 | 0x2B4D8 | Good spore creation | 0x01 |
| 0x11 | 0x2BE08, 0x2E1CC | Good spore pickup, Level won | 0x01 |
| 0x12 | 0x2E1EA (See 0x2E438) | Level lost Marks the slot in $598 | 0x02 |
| 0x13 | 0x2B610 | Unknown timer $59C triggered | 0x02 |
| 0x14 | 0x29940 (+ conditional at 0x2B21A) | Life lost | 0x01 |
| 0x15 | 0x29D14 | Send guardian to Pandaemonium sequence | 0x01 |
| 0x16 | 0x2960E | Game over triggered (lost all lives, Beast appears) | 0x01 |
| 0x17 | 0x2ADCA | Guardian shot during night | 0x01 |
| 0x18 | 0x2ADBC (+ conditional at 0x2B21A) | Guardian shot during day | 0x01 |
| 0x19 | 0x2AD50, 0x2BF08 | Evil-spore destroyed | 0x01 |
Events $06 and $0A do not appear in the code. I am also unsure about event $11 (seems to be both a good spore pickup at address 2BE08, and level won at 2E1CC), or event $13 (as I don’t know yet what timer $59C triggers).
These event IDs trigger gameplay-specific sound effects or audio cues. The events are queued into the audio buffer if there is space or if they have sufficient priority to evict a lower-priority sound.
For example, address 2BE04 within SUB_ProcessPowerCrystal_2BD58 queues event ID $11 after verifying a successful good spore pickup.
- Decoding Logic in SUB_2E3CA:
- The subroutine searches for a matching event (CMP.B 0(A1,D1.W),D0 at 2E3E8), an empty slot (CMPI.B #$ff,0(A1,D1.W) at 3E3F6), or a lower-priority slot (CMP.B 0(A2,D3.W),D2 at 2E414).
- Priority is compared using BHI.W (Branch if Higher), meaning a higher priority value (e.g., $03) is lower priority than $01, allowing overwrite.
- Implications:
- $05 (Servant shot) has the highest priority ($00).
- Most events ($00–$04, $07, $0C–$19) share $01 or $02, indicating moderate to high priority, with $02 for less critical events (e.g., $08, $09, $0B).
Conclusion
The four-byte buffer at 39290 serves as a dynamic queue for up to four sound events, each tied to a specific gameplay moment and played through the Amiga’s four audio channels. With a priority system, the game ensures that when the action starts to get frantic, critical sounds like the servant being shot (with a top priority of $00) take precedence, while less urgent effects make way when slots fill up.

Leave a comment