Sound effects event system

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 0
  • 39292 — slot 1
  • 39294 — slot 2
  • 39296 — 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:

  1. Looks up the priority for the incoming event.
  2. Scans the event buffer for a matching ID to avoid duplication.
  3. Searches for an unused slot (value $FF).
  4. 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:
AddressPriority Data (Hex)Event ID
3927601$00
3927702$01
3927801$02
3927901$03
3927A01$04
3927B00 (Highest)$05
3927C01($06)
3927D02$07
3927E02$08
3927F01$09
3928003 (Lowest)($0A)
3928101$0B
3928201$0C
3928301$0D
3928401$0E
3928501$0F
3928601$10
3928701$11
3928802$12
3928902$13
3928A01$14
3928B01$15
3928C01$16
3928D01$17
3928E01$18
3928F01$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 $05 has 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:

RoleInstructionWhat it does
Duplicate / matching scanMOVEQ #3,D1 … DBF D1,SearchForMatchingEventWalks 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 scanreset D1 to 3, test each byte against $FFFinds the first unused slot, by looking for the first $FF (unused entry).
Priority lookupLEA LAB_39276,A2 / MOVE.B 0(A2,D0.W),D2Grabs the incoming event’s priority byte. Compare incoming priority (D2) with the existing slot’s priority (0(A2,D3.W))
Replace-lower-prioritycompare D2 with 0(A2,D3.W) per slotOverwrites an existing lower-priority event if necessary, if the new value is higher
Final writeMOVE.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:

  1. 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.
  2. Empty slot scan:
    CMPI.B #$ff,0(A1,D1.W)
    Scans for any $FF entry, marking it as unused.
  3. 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_39294 iterates over the same 4-byte buffer.
  • If a slot is not $FF, it plays the corresponding sound effect using SUB_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), and DMACON (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 IDAddress that sets D0Inference/SpeculationPriority
0x000x2D9C8Central face opens for tarot0x01
0x010x2B57CEvil spore converted into a portal0x02
0x020x2AA92Good spore dropped on good growth, converted into a good portal0x01
0x030x2B2CETrigger for power crystal?0x01
0x040x28B9CGrave screen initialization is complete following game over0x01
0x050x2AE00Guardian’s servant shot0x00
0x070x2AB42, 0x2ABEEBullet(s) fired – single and burst variants0x01
0x080x2A85CLevel starts
Guardian initial spawn (sets sprite & movement slot)
0x02
0x090x2D9F6Day/night change0x02
0x0B0x2B124Evil spore created0x02
0x0C0x2B2EACentral face closed (FaceAnimationCounter_434 Reaches 658 $292)0x01
0x0D0x2BF54Energy update – gain / loss 0x01
0x0ESee 2E42CCentral face opens, Power Crystal appears
(FaceAnimationCounter_434 Reaches 544)
0x01
0x0F0x2BE30Power crystal collected0x01
0x100x2B4D8Good spore creation0x01
0x110x2BE08, 0x2E1CCGood spore pickup, Level won0x01
0x120x2E1EA
(See 0x2E438)
Level lost
Marks the slot in $598
0x02
0x130x2B610Unknown timer $59C triggered0x02
0x140x29940 (+ conditional at 0x2B21A)Life lost0x01
0x150x29D14Send guardian to Pandaemonium sequence0x01
0x160x2960EGame over triggered (lost all lives, Beast appears)0x01
0x170x2ADCAGuardian shot during night0x01
0x180x2ADBC (+ conditional at 0x2B21A)Guardian shot during day0x01
0x190x2AD50, 0x2BF08Evil-spore destroyed0x01

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

Create a website or blog at WordPress.com

Up ↑