Skip to content

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:

  1. Bridge — instantiated cue that detects the event
  2. 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).

<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”.

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>
  • Must be instantiate="true" to fire repeatedly on the event
  • Can’t do entity writes — but CAN signal another cue
  • $busy check prevents Worker from being signalled while it’s running
  • instantiate="false" lets it do entity writes
  • Receives event.param from the Bridge’s signal_cue_instantly
  • Must call reset_cue cue="this" — otherwise next signal fires “no listener” warning
  • signal_cue_instantly is required (NOT plain signal_cue — that one drops param=)
  • Clears $busy after a 2-second delay
  • Without it, $busy stays 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.

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.

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.

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.

<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.

<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.

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
  • $busy flags in factionlogic_economy for 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.

  • Cue — the canonical reference
  • Library — alternative for shared logic
  • Action — categorical action reference
  • Workflow — debug.log patterns to spot drops