CTLD has two distinct event channels, and they never overlap:
| Channel | Class | Carries | Source page |
|---|---|---|---|
| Business events | EventDispatcher |
CTLD domain events (OnCrateLoaded, OnTroopsDeployed, …) |
this page |
| DCS engine events | CTLDDCSEventBridge |
Raw S_EVENT_* from the simulator |
see Architecture |
EventDispatcher is the internal pub/sub bus that managers use to signal domain
changes to each other and to external mission scripts. DCS engine events (S_EVENT_DEAD,
S_EVENT_BIRTH, …) never pass through it — they arrive via CTLDDCSEventBridge, are
handled by the relevant manager, and only then re-emitted as business events where useful.
EventDispatcher is a singleton declared in src/CTLD_core.lua. Obtain it with
getInstance(); there is no :new() entry point for it.
local ed = EventDispatcher.getInstance()Registers a callback for a named event. The callback receives the event payload table as its single argument.
EventDispatcher.getInstance():subscribe("OnCrateLoaded", function(evt)
-- evt.crateName, evt.carrierUnitName, evt.coalition, ...
end)The same callback may be registered more than once; each registration produces one
additional call on publish. Callers are responsible for avoiding accidental duplicate
subscriptions. A non-function callback is silently ignored.
Removes a previously registered callback. The callback argument must be the exact same
function reference used at subscribe time (keep a local reference if you intend to
unsubscribe). Only the last-registered matching occurrence is removed per call.
local function onLoaded(evt) --[[ ... ]] end
local ed = EventDispatcher.getInstance()
ed:subscribe("OnCrateLoaded", onLoaded)
-- later:
ed:unsubscribe("OnCrateLoaded", onLoaded)Drops every subscriber for one event — used on module teardown.
EventDispatcher.getInstance():unsubscribeAll("OnCrateLoaded")Dispatches payload to every subscriber of eventName.
EventDispatcher.getInstance():publish("OnMySomethingHappened", {
unitName = "UH-1H-1",
coalition = coalition.side.BLUE,
timestamp = timer.getAbsTime(),
})Two guarantees matter for authors:
- Callback isolation. Each subscriber is invoked in a
pcall. A callback that errors is logged (ERROR,EventDispatcher:publish [<event>] callback error: …) and does not abort the remaining subscribers. - Re-entrancy safety. The subscriber list is copied before iteration, so a callback may
safely
subscribeorunsubscribeduring dispatch without affecting the in-flight dispatch.
Publishing an event with no subscribers is a no-op (cheap early return).
By convention every CTLD event carries a timestamp field set to timer.getAbsTime().
Beyond that, fields are event-specific — see the catalogue below. Managers publish through
thin private helpers (CTLDCrateManager:_publish, CTLDJTACManager:_publishEvent) that
forward to EventDispatcher:publish; subscribers see no difference.
v1 used a single catch-all ctld.addCallback with an if event.id == … chain. v2 replaces
it with targeted subscriptions — see
Migration v1 → v2 for the full worked example.
-- v2: one subscription per event of interest, no id dispatch chain
EventDispatcher.getInstance():subscribe("OnCrateSpawned", function(evt)
-- evt.crateName, evt.coalition, evt.spawnedBy, evt.position
end)Events are grouped by publishing domain. Every payload also carries timestamp (omitted
from the tables unless it is the only field). "Published by" names the manager and the
method(s) that emit the event, verified against src/.
Almost all event names use the On<Something> convention. The two exceptions are the RECON
FARP events (ReconFarpDetected, ReconFarpLost), which have no On prefix — subscribe to
them by their exact names.
A radio beacon is deployed (by a player or from a zone).
| Field | Type | Description |
|---|---|---|
player |
string | Player name, or "MissionMaker" if from a zone |
playerUnit |
Unit or nil | DCS Unit object |
coalition |
number | Coalition ID |
beacon |
table | Full beacon payload (see note) |
beacon sub-table: { beaconName, name, position (vec3), mgrsCoords, frequencies { vhf, uhf, fm }, battery { remainingTime, wasInfinite, duration, infinite } }
Published by: CTLDBeaconManager:dropBeacon(), :createAtZone()
A player manually removes the closest beacon.
| Field | Type | Description |
|---|---|---|
player |
string | |
playerUnit |
Unit | |
coalition |
number | |
beacon |
table | { beaconName, name, position, mgrsCoords, frequencies, battery, distance } |
reason |
string | "manual" |
frequenciesFreed |
table | { vhf, uhf, fm } |
Published by: CTLDBeaconManager:removeClosestBeacon()
A beacon's battery runs out, or its DCS units are destroyed.
| Field | Type | Description |
|---|---|---|
beacon |
table | { beaconName, name, position, mgrsCoords, frequencies, battery, unitsAlive, durationAlive } |
reason |
string | "battery" or "units" |
frequenciesFreed |
table | { vhf, uhf, fm } |
Published by: CTLDBeaconManager:_refreshAll() (periodic tick — battery depleted or too few radio units alive)
Emitted on each beacon-manager tick with a summary of all active beacons.
| Field | Type | Description |
|---|---|---|
beacons |
table | Array of beacon payload tables |
totalBeaconsRefreshed |
number | |
totalBeaconsDestroyed |
number | Beacons destroyed this tick |
Published by: CTLDBeaconManager:_refreshAll()
A player toggles the beacon F10 map layer.
| Field | Type | Description |
|---|---|---|
player |
string | |
playerUnit |
Unit | |
coalition |
number | |
previousState |
bool | |
newState |
bool | |
action |
string | "enabled" or "disabled" |
beaconsDisplayed |
table | Array of beacons now displayed |
totalBeaconsDisplayed |
number |
Published by: CTLDBeaconManager:toggleLayer()
Any crate appears on the map.
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | Internal crate object |
crateName |
string | Unique crate identifier |
descriptor |
table | Crate descriptor |
position |
vec3 | |
coalition |
number | |
spawnedBy |
string or nil | Player / mission-maker name |
spawnMethod |
string | "request_crate", "parachute_drop", "slingload_cut", "mm_late_activation", … |
Published by: CTLDCrateManager:spawnCrate(), :unloadCrate(), :cutSlingload(), :parachuteCrates()
A crate is picked up (slingload or DCS native cargo).
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
carrierUnitName |
string | Transport unit name |
coalition |
number | |
descriptor |
table | |
trigger |
string | "slingload" or "dcs_native" |
method |
string or nil | "dcs_native" (absent for slingload) |
Published by: CTLDCrateManager:loadCrate(), :checkHoverStatus(), :_checkNativeDCSCargo()
A crate is dropped or unloaded (not unpacked).
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
coalition |
number | |
descriptor |
table | |
method |
string or nil | "dcs_native" if native, absent for slingload |
Published by: CTLDCrateManager:unloadCrate(), :dropCrate(), :cutSlingload(), :_checkNativeDCSCargo()
A crate is destroyed by overspeed slingload or cut-slingload impact.
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
coalition |
number | |
transport |
Unit | The carrying aircraft |
trigger |
string | "slingload_overspeed" or "slingload_cut_impact" |
Published by: CTLDCrateManager:checkHoverStatus(), :cutSlingload()
A crate is removed from the world (unpacked, destroyed, or loaded natively).
| Field | Type | Description |
|---|---|---|
crateName |
string | |
position |
vec3 | Last known position |
coalition |
number | |
descriptor |
table | |
reason |
string | "destroyed", "unpacked", or "loaded" |
Published by: CTLDCrateManager:cutSlingload(), :loadCrate(), :unpackCrate(), :destroyCrate()
A crate is successfully unpacked (assembled into equipment).
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
descriptor |
table | |
position |
vec3 | Unpack location |
coalition |
number | |
carrierUnitName |
string or nil | Unit performing the unpack |
Published by: CTLDCrateManager:unpackCrate()
A crate is destroyed on high-altitude drop impact (distinct from slingload loss).
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
coalition |
number | |
reason |
string | "drop_impact" |
Published by: CTLDCrateManager:dropCrate() (impact path)
A crate is released in parachute-drop mode.
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
descriptor |
table | |
dropPosition |
vec3 | Position at release |
landingPosition |
vec3 | Estimated landing position |
altitude |
number | AGL in metres |
descentTime |
number | Estimated seconds to landing |
carrierUnitName |
string | |
player |
string |
Published by: CTLDCrateManager:parachuteCrates()
A parachuted crate lands (async, after the descent poll).
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
descriptor |
table | |
position |
vec3 | Landing position |
coalition |
number | |
startAltitude |
number | AGL at release |
Published by: CTLDCrateManager:parachuteCrates() (landing-poll callback)
A mission-maker pre-placed crate is registered (including on late activation).
| Field | Type | Description |
|---|---|---|
crate |
CTLDCrate | |
crateName |
string | |
descriptor |
table | |
position |
vec3 | |
coalition |
number |
Published by: CTLDCrateManager:registerMMCrate()
A vehicle is spawned in WAITING state, ready for transport (Request Equipment).
| Field | Type | Description |
|---|---|---|
vehicleId |
string | Internal CTLD vehicle ID |
vehicle |
Unit | DCS Unit object |
vehicleType |
string | DCS type name |
spawner |
string or nil | Player name |
logisticZone |
table or nil | Zone where spawned |
spawnMethod |
string | "request_vehicle" |
position |
vec3 |
Published by: CTLDVehicleSpawner:spawnVehicleForTransport()
A vehicle is loaded into a transport.
| Field | Type | Description |
|---|---|---|
vehicleId |
string | |
ctldVehicleObject |
CTLDVehicle | |
dcsUnitObject |
Unit or nil | Present if native load |
vehicleType |
string | |
transportUnitObject |
Unit | |
player |
string | |
method |
string | "slingload" or "dcs_native" |
spawnMethod |
string | |
position |
vec3 | |
transportPosition |
vec3 |
Published by: CTLDVehicleSpawner:loadVehicle()
A vehicle is unloaded from a transport. Same fields as OnVehicleLoaded.
Published by: CTLDVehicleSpawner:unloadVehicle()
A vehicle is packed into crates.
| Field | Type | Description |
|---|---|---|
vehicleType |
string | |
descriptor |
table | Crate descriptor used |
transport |
string | Transport unit name |
player |
string or nil | |
cratesSpawned |
number | Number of crates spawned |
Published by: CTLDVehicleSpawner:packVehicle()
A vehicle is released in parachute mode.
| Field | Type | Description |
|---|---|---|
vehicle |
CTLDVehicle | |
transport |
string | Transport unit name |
player |
string | |
altitude |
number | AGL in metres |
dropPosition |
vec3 | |
estimatedLandingPos |
vec3 | |
estimatedLandingTime |
number | timer.getAbsTime() + descentTime |
Published by: CTLDVehicleSpawner:parachuteVehicle()
A parachuted vehicle lands.
| Field | Type | Description |
|---|---|---|
vehicle |
CTLDVehicle | |
position |
vec3 | |
transport |
string | |
player |
string | |
startAltitude |
number | AGL at release |
Published by: CTLDVehicleSpawner:parachuteVehicle() (landing callback)
A tracked CTLD vehicle is destroyed.
| Field | Type | Description |
|---|---|---|
vehicleId |
string | |
vehicle |
Unit | S_EVENT_DEAD initiator |
vehicleType |
string | |
coalition |
number or nil | |
position |
vec3 | |
durationAlive |
number | Seconds since spawn |
Published by: CTLDVehicleSpawner:onDead() (S_EVENT_DEAD handler)
A CTLD-tracked ground unit appears or disappears. These two are cross-cutting — both the
vehicle spawner and the crate manager publish OnGroundUnitSpawned.
| Field | Type | Description |
|---|---|---|
vehicleType |
string | DCS type name |
position |
vec3 | |
coalitionId |
number | (OnGroundUnitSpawned only) |
reason |
string | (OnGroundUnitRemoved only) "loaded", "packed", "dead" |
Published by:
OnGroundUnitSpawned:CTLDVehicleSpawner:_spawnGroundUnit(),CTLDCrateManager:_spawnUnpacked()OnGroundUnitRemoved:CTLDVehicleSpawner:loadVehicle(),:onDead(),:packVehicle()
Troops are deployed by parachute.
| Field | Type | Description |
|---|---|---|
troops |
CTLDTroopGroup | |
carrierUnitName |
string | |
player |
string | |
trigger |
string | "parachute" |
destination |
table | { type="combat", troopZone=nil } |
Published by: CTLDTroopManager:parachuteTroops()
Parachuted troops land (async, after the descent timer).
| Field | Type | Description |
|---|---|---|
troops |
CTLDTroopGroup | |
spawnedGroup |
Group | DCS group spawned on the ground |
positions |
table | Array of vec3, one per soldier |
transport |
string | |
player |
string | |
startAltitude |
number |
Published by: CTLDTroopManager:parachuteTroops() (landing callback)
A JTAC is spawned — vehicle, drone, or infantry soldier within a troop group.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.groupId |
number | |
jtac.unitName |
string | |
jtac.unitId |
number or nil | |
jtac.unitType |
string or nil | |
jtac.coalition |
number | |
jtac.position |
vec3 or nil | |
jtac.isFlying |
bool | true for drone JTACs |
jtac.isInfantry |
bool | |
jtac.route |
table or nil | Drone patrol route |
spawner |
string | Player unit name or "MissionMaker" |
laserCode |
number | Assigned laser code (0 if none) |
smokeEnabled |
bool | |
smokeColor |
string or nil | |
lockMode |
string | JTAC lock-mode setting |
radio |
table or nil | { freq, modulation } |
Published by: CTLDJTACManager:spawnJTAC(), :startLaseTroopUnit() (infantry JTAC)
A vehicle JTAC is embarked into a transport.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.coalition |
number | |
transport |
Unit |
Published by: CTLDJTACManager:setJTACInTransit()
A JTAC acquires a target and begins lasing.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.unitName |
string | |
jtac.position |
vec3 | |
jtac.coalition |
number | |
target.unitName |
string | |
target.unitId |
number | |
target.unitType |
string | |
target.coalition |
number | |
target.position |
vec3 | |
target.priority |
number | Target priority score |
target.selectionMethod |
string | "auto_nearest" or "manual" |
target.wasManuallySelected |
bool | |
laserCode |
number | |
lockMode |
string | |
distance |
number | Metres from JTAC to target |
lineOfSight |
bool | LOS confirmed at time of lock |
radio |
table or nil | |
message |
string or nil | Status message shown to player |
Published by: CTLDJTACManager:_autoLaseLoop() (on acquisition)
Emitted each lase cycle while a JTAC is actively lasing.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.unitName |
string | |
jtac.coalition |
number | |
target.unitName |
string | |
target.position |
vec3 | Corrected position for drones |
laserCode |
number |
Published by: CTLDJTACManager:_autoLaseLoop() (each cycle)
A JTAC stops lasing.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.coalition |
number | |
jtac.laserCode |
number | |
target.unitName |
string or nil | |
target.unitType |
string | |
target.lastKnownPosition |
vec3 | |
reason |
string | "target_dead", "out_of_range", "manual" |
Published by: CTLDJTACManager:_stopLaseAndPublish()
A drone JTAC starts its orbit over a target.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.coalition |
number | |
target.unitName |
string | |
target.position |
vec3 |
Published by: CTLDJTACManager:_updateOrbit()
A JTAC smokes its current target on player request.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.coalition |
number | |
target.unitName |
string | |
target.position |
vec3 | |
smokePosition |
vec3 | Actual smoke spawn point |
smokeColor |
number | trigger.smokeColor.* constant |
Published by: CTLDJTACManager:requestSmoke()
A JTAC is destroyed in combat.
| Field | Type | Description |
|---|---|---|
jtac.groupName |
string | |
jtac.coalition |
number | |
jtac.laserCode |
number | |
killer |
table or nil | { unitName, playerName } |
lastTarget |
table or nil | Last lased target |
Published by: CTLDJTACManager:killJTAC() (S_EVENT_DEAD handler)
Packing a JTAC vehicle is not a death —
OnJTACDeaddoes not fire when a JTAC vehicle is re-embarked.
A player in RECON detects an enemy FARP or FOB. (No On prefix — historic name.)
| Field | Type | Description |
|---|---|---|
player |
string | Player name |
playerUnit |
Unit | DCS Unit object |
coalition |
number | Enemy coalition ID |
playerCoalition |
number | Player's coalition ID |
position |
vec3 | FARP/FOB position |
id |
string | Internal mark ID (matched by ReconFarpLost) |
Published by: CTLDReconManager:_syncFarpMarks()
A previously detected enemy FARP/FOB leaves the scan radius or is destroyed. (No On
prefix.)
| Field | Type | Description |
|---|---|---|
player |
string | |
id |
string | Mark ID matching the ReconFarpDetected event |
Published by: CTLDReconManager:_syncFarpMarks()
A player performs a RECON scan.
| Field | Type | Description |
|---|---|---|
player |
string | |
playerUnit |
Unit | |
coalition |
number | |
position |
vec3 | Scanner position |
altitude |
number | AGL in metres |
searchRadius |
number | Metres |
activeLayers |
table | Array of active layer descriptors |
targets |
table | Array of detected targets |
targetsByLayer |
table | Map layerId → targets |
totalTargetsDetected |
number | |
totalMarksCreated |
number |
Published by: CTLDReconManager:scan()
Emitted each auto-refresh cycle with delta information.
| Field | Type | Description |
|---|---|---|
player |
string | |
playerUnit |
Unit | |
coalition |
number | |
position |
vec3 | |
altitude |
number | |
activeLayers |
table | |
targets |
table | All current targets |
newTargets |
table | Newly detected this cycle |
movedTargets |
table | Targets that moved |
lostTargets |
table | Targets that disappeared |
totalTargetsCurrent |
number | |
totalTargetsNew |
number | |
totalTargetsMoved |
number | |
totalTargetsLost |
number |
Published by: CTLDReconManager:_doRefresh() (auto-refresh timer)
A player stops RECON and all marks are removed.
| Field | Type | Description |
|---|---|---|
player |
string | |
playerUnit |
Unit | |
coalition |
number | |
marksRemoved |
table | Array of removed mark records |
totalMarksRemoved |
number | |
refreshStopped |
bool | Whether auto-refresh was running |
Published by: CTLDReconManager:stopScan()
A player enables/disables a RECON detection layer.
| Field | Type | Description |
|---|---|---|
layerId |
string | e.g. "infantry", "ground_vehicles" |
layerName |
string | Human-readable layer name |
visible |
bool | New state |
coalition |
number | |
player |
string |
Published by: CTLDReconManager:toggleLayer()
A player enables or disables RECON auto-refresh.
| Field | Type | Description |
|---|---|---|
player |
string | |
playerUnit |
Unit | |
coalition |
number | |
previousState |
bool | |
newState |
bool | |
targetsCount |
number | Current target count |
refreshInterval |
number | Seconds between refreshes |
Published by: CTLDReconManager:enableAutoRefresh(), :disableAutoRefresh()
A FOB is fully assembled and deployed.
| Field | Type | Description |
|---|---|---|
fob.fobId |
string | |
fob.name |
string | |
fob.coalitionId |
number | |
cratesUsed |
table | Array of crates consumed |
totalCratesUsed |
number | |
position |
vec3 | |
sceneObjects |
table | DCS StaticObjects spawned |
logisticZone |
table | Created LGZ { name, radius, type="static" } |
player |
string | Player unit name who triggered deployment |
Published by: CTLDFOBManager:_registerDeployedFOB()
A FOB's integrity drops below the destruction threshold.
| Field | Type | Description |
|---|---|---|
fob.fobId |
string | |
fob.name |
string | |
fob.coalitionId |
number | |
position |
vec3 | |
destruction.killerUnit |
Unit or nil | |
destruction.killerCoalition |
number or nil | |
destruction.objectsDestroyed |
number | |
destruction.objectsTotal |
number | |
destruction.destructionThreshold |
number | Config fobDestructionThreshold (default 0.5) |
destruction.integrityPercent |
number | 0.0–1.0 at time of event |
logisticZone |
table | { name, wasActive=true } |
durationAlive |
number | Seconds from deployment to destruction |
Published by: CTLDFOBManager:_destroyFOB() (from the S_EVENT_DEAD handler onDead())
An AA system is fully assembled.
| Field | Type | Description |
|---|---|---|
systemName |
string | "HAWK", "PATRIOT", … |
groupName |
string | DCS group name |
heli |
Unit or nil | nil if AI-deployed |
coalition |
number | |
position |
vec3 |
Published by: CTLDCrateAssemblyManager:spawnSystemAt(), :_assemble()
A launcher is added to an existing AA system.
| Field | Type | Description |
|---|---|---|
systemName |
string | |
groupName |
string | |
heli |
Unit | |
coalition |
number |
Published by: CTLDCrateAssemblyManager:_rearm()
A full AA system is repaired and re-spawned. Same fields as OnAASystemRearmed.
Published by: CTLDCrateAssemblyManager:_repair()
Emitted on each smoke-refresh timer cycle.
| Field | Type | Description |
|---|---|---|
troopZones |
table | Array of troop-zone payload tables |
logisticZones |
table | Array of logistic-zone payload tables |
refreshInterval |
number | Seconds |
Published by: CTLDZoneManager:_scheduleSmoke() (periodic timer)
Dynamic zone units are added or removed.
| Field | Type | Description |
|---|---|---|
zones |
table | All zones after update |
unitsAdded |
table | Newly registered units |
unitsRemoved |
table | Units that left the zone |
Published by: CTLDZoneManager:_publishLogisticZoneUpdated()
| Domain | Count |
|---|---|
| Beacon | 5 |
| Crate | 10 |
| Vehicle | 9 |
| Troop | 2 |
| JTAC | 8 |
| RECON | 8 |
| FOB | 2 |
| AA System | 3 |
| Zone | 2 |
| Total | 49 |
Roughly ten events have internal subscribers (menu refresh and cross-manager coordination); the rest are published purely for mission-maker consumption and have no internal listeners until you subscribe to them.
DCS engine events do not flow through EventDispatcher. A single
CTLDDCSEventBridge singleton registers one world.addEventHandler and routes each
S_EVENT_* to the managers that registered for it, via
bridge:register(target, eventId, "methodName"). The bridge does no filtering beyond the
event id — each manager filters internally. See Architecture for the
CTLDDCSEventBridge design.
The engine events currently handled, and by whom:
| DCS event | Handler(s) |
|---|---|
S_EVENT_PLAYER_ENTER_UNIT |
CTLDPlayerTracker:onPlayerEnterUnit(), CTLDPlayerManager:onPlayerEnterUnit() |
S_EVENT_PLAYER_LEAVE_UNIT |
CTLDPlayerTracker:onPlayerLeaveUnit(), CTLDPlayerManager:onPlayerLeaveUnit() |
S_EVENT_BIRTH |
CTLDPlayerTracker:onBirth(), CTLDCrateManager:onBirth(), CTLDJTACManager:onBirth(), CTLDVehicleSpawner:onBirth() |
S_EVENT_LAND |
CTLDPlayerManager:onLand(), CTLDCoreManager:onAILand() |
S_EVENT_TAKEOFF |
CTLDPlayerManager:onTakeoff() |
S_EVENT_DEAD |
CTLDTroopManager:onUnitDead() + :onTransportDead(), CTLDVehicleSpawner:onDead(), CTLDCrateManager:onCrateDead(), CTLDFOBManager:onDead(), CTLDZoneManager:onDead() |
S_EVENT_STATIC_DEAD— a synthetic event.S_EVENT_DEADis unreliable for static and base objects.CTLDStaticWatcherpolls registered objects on a 1 s timer and, when one disappears, dispatches a syntheticS_EVENT_STATIC_DEAD(payload{ id, meta }) throughEventDispatcher. It rides the business bus but is neither a DCS engine event nor anOn*domain event.