Bridge + Worker pattern
The Bridge + Worker pattern is the canonical race-avoidance idiom for MD cues that need to write to entities (add_inventory, destroy_object, modifying NPC state). It splits the cue into two parts:
- Bridge — instantiated cue that detects the event
- Worker — non-instantiated cue that does the actual work
Without this split, instantiate="true" cues silently drop entity writes.
This pattern shows up in mlog_bmb, mlog_frs, mlog_heroes, and is referenced from Cue (the canonical home).
The problem
Section titled “The problem”<cue name="WatchAndReward" instantiate="true"> <conditions> <event_object_destroyed group="$tracked"/> </conditions> <actions> <!-- ⚠ This silently fails: --> <add_inventory ware="ware.inv_X" exact="1" entity="event.param"/> </actions></cue>The action add_inventory is engine-determined to be incompatible with instantiate="true". The engine silently drops it. No error in debug.log. The player gets no inventory item. You spend hours debugging “why doesn’t my reward work”.
The solution
Section titled “The solution”Split into Bridge + Worker + ReleaseBusy:
<!-- Bridge: instantiated, detects event, signals Worker --><cue name="Bridge" instantiate="true"> <conditions> <event_object_destroyed group="$tracked"/> <check_value value="not $busy"/> </conditions> <actions> <set_value name="$busy" exact="true"/> <signal_cue_instantly cue="Worker" param="event.param"/> </actions></cue>
<!-- Worker: NON-instantiated, does the real entity writes --><cue name="Worker" instantiate="false"> <conditions> <event_cue_signalled cue="Bridge"/> </conditions> <actions> <add_inventory ware="ware.inv_X" exact="1" entity="event.param"/>
<reset_cue cue="this"/> </actions></cue>
<!-- ReleaseBusy: clears the lock after delay --><cue name="ReleaseBusy" delay="2s"> <conditions> <event_cue_signalled cue="Worker"/> </conditions> <actions> <set_value name="$busy" exact="false"/> <reset_cue cue="this"/> </actions></cue>Why each piece is needed
Section titled “Why each piece is needed”Bridge (instantiated)
Section titled “Bridge (instantiated)”- Must be
instantiate="true"to fire repeatedly on the event - Can’t do entity writes — but CAN signal another cue
$busycheck prevents Worker from being signalled while it’s running
Worker (not instantiated)
Section titled “Worker (not instantiated)”instantiate="false"lets it do entity writes- Receives
event.paramfrom the Bridge’ssignal_cue_instantly - Must call
reset_cue cue="this"— otherwise next signal fires “no listener” warning signal_cue_instantlyis required (NOT plainsignal_cue— that one dropsparam=)
ReleaseBusy
Section titled “ReleaseBusy”- Clears
$busyafter a 2-second delay - Without it,
$busystays true and Bridge stops firing - Delay prevents same-frame Bridge re-fire after Worker finishes
Why not just a single non-instantiated cue?
Section titled “Why not just a single non-instantiated cue?”<!-- ❌ This only fires ONCE --><cue name="Worker" instantiate="false"> <conditions> <event_object_destroyed group="$tracked"/> </conditions> <actions> <add_inventory ... /> </actions></cue>Non-instantiated cues fire once, then stay completed. Subsequent destruction events get “no corresponding listeners” warning. The Bridge pattern’s signal_cue_instantly + reset_cue keeps the Worker re-fireable.
Variants
Section titled “Variants”Without ReleaseBusy (single-shot)
Section titled “Without ReleaseBusy (single-shot)”If you only need the cue to fire once per game session:
<cue name="Bridge" instantiate="true"> <conditions> <event_object_destroyed group="$tracked"/> <check_value value="not @$done"/> </conditions> <actions> <set_value name="$done" exact="true"/> <signal_cue_instantly cue="Worker" param="event.param"/> </actions></cue>Use $done as a one-shot flag.
Multiple Workers (different action types)
Section titled “Multiple Workers (different action types)”If you need multiple entity-write actions in response to one event, use multiple Workers:
<cue name="Bridge" instantiate="true"> <conditions>...</conditions> <actions> <signal_cue_instantly cue="WorkerA" param="event.param"/> <signal_cue_instantly cue="WorkerB" param="event.param"/> </actions></cue>Each Worker handles its own action type.
Common mistakes
Section titled “Common mistakes”Using signal_cue instead of signal_cue_instantly
Section titled “Using signal_cue instead of signal_cue_instantly”<signal_cue cue="Worker" param="$value"/> <!-- ❌ param dropped -->Plain signal_cue is queued — param= doesn’t propagate. Worker receives event.param = null. Always use signal_cue_instantly when you need to pass data.
Forgetting reset_cue
Section titled “Forgetting reset_cue”<cue name="Worker" instantiate="false"> <conditions> <event_cue_signalled cue="Bridge"/> </conditions> <actions> <add_inventory .../> <!-- ❌ no reset_cue — fires once, then dead --> </actions></cue>Without <reset_cue cue="this"/>, the Worker fires once then stays complete. Subsequent signals get warned.
$busy lock without ReleaseBusy
Section titled “$busy lock without ReleaseBusy”<set_value name="$busy" exact="true"/><!-- ❌ never cleared — Bridge stops firing forever -->If you set $busy without scheduling its release, the Bridge gets stuck. Always pair with ReleaseBusy or equivalent.
Where Bridge + Worker is used in vanilla
Section titled “Where Bridge + Worker is used in vanilla”Vanilla doesn’t use this exact pattern by name, but does use:
- Cue tree with signal-passing in
boarding.xml,notifications.xml - Reset cue pattern in mission cleanup
$busyflags infactionlogic_economyfor state machines
Vanilla relies more on the operation datatype (event_boarding_operation_X) for state. Mods that don’t have that engine integration use Bridge + Worker.