Files
MOOSE/Moose Development/Moose/Python/MooseBridgeDcsEventsExtension.lua
T
2026-08-17 22:56:18 +02:00

317 lines
13 KiB
Lua

--- DCS world event forwarding for MOOSE_BRIDGE.
--
-- Load after MooseBridge.lua and before constructing/starting the bridge.
-- DCS events are normalized here; Python never needs to understand the raw
-- world event table or MOOSE EVENTDATA implementation details.
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgeDcsEventsExtension.lua") end
local function bridge_event_available(event_id)
return type(event_id) == "number" and event_id > 0
end
--- Cache current DCS ownership for all known MOOSE AIRBASE objects.
-- The DCS BaseCaptured event already exposes the new owner, so the cache is
-- needed to include the previous owner in the normalized bridge event.
function MOOSE_BRIDGE:_CacheAirbaseCoalitions()
self.AirbaseCoalitions = self.AirbaseCoalitions or {}
if not _DATABASE or type(_DATABASE.AIRBASES) ~= "table" then return self end
for airbase_name, airbase in pairs(_DATABASE.AIRBASES) do
local ok, result = pcall(function()
local name = self:_SafeCall(airbase, "GetName") or airbase.AirbaseName or airbase_name
local owner = self:_CoalitionToName(self:_SafeCall(airbase, "GetCoalition"))
return name and {object_id="AIRBASE:" .. tostring(name), coalition=owner} or nil
end)
if ok and result and result.object_id then
self.AirbaseCoalitions[result.object_id] = result.coalition
elseif not ok then
self:_Log("Failed to cache airbase " .. tostring(airbase_name) .. ": " .. tostring(result))
end
end
return self
end
--- Resolve the authoritative MOOSE AIRBASE wrapper from an EVENTDATA object.
function MOOSE_BRIDGE:_AirbaseFromCapturedEvent(EventData)
if type(EventData) ~= "table" then return nil, nil end
local place = EventData.Place
local name = EventData.PlaceName
if not name and place then
name = self:_SafeCall(place, "GetName")
end
if not name and EventData.place then
local ok, value = pcall(function() return EventData.place:getName() end)
if ok then name = value end
end
if not name then return nil, nil end
local airbase = type(place) == "table" and place or nil
if _DATABASE and type(_DATABASE.AIRBASES) == "table" then
airbase = _DATABASE.AIRBASES[name] or airbase
end
if not airbase and AIRBASE and AIRBASE.FindByName then
local ok, value = pcall(function() return AIRBASE:FindByName(name) end)
if ok then airbase = value end
end
return airbase, tostring(name)
end
--- Subscribe to selected low-frequency DCS events through MOOSE.
function MOOSE_BRIDGE:_StartDcsEventForwarding()
if self.DcsEventForwardingStarted then return self end
if not EVENTS or not self.HandleEvent then
self:_Log("DCS event forwarding unavailable")
return self
end
self.DcsRegisteredEvents = {}
if bridge_event_available(EVENTS.BaseCaptured) then
self:_CacheAirbaseCoalitions()
self:HandleEvent(EVENTS.BaseCaptured)
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.BaseCaptured
self:_Log("DCS BaseCaptured event forwarding enabled")
end
if bridge_event_available(EVENTS.UnitLost) then
self:HandleEvent(EVENTS.UnitLost)
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.UnitLost
self:_Log("DCS UnitLost event forwarding enabled")
end
if bridge_event_available(EVENTS.Dead) then
self:HandleEvent(EVENTS.Dead)
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.Dead
self:_Log("DCS Dead event forwarding enabled")
end
if bridge_event_available(EVENTS.Kill) then
self:HandleEvent(EVENTS.Kill)
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.Kill
self:_Log("DCS Kill event forwarding enabled")
end
if bridge_event_available(EVENTS.MissionEnd) then
self:HandleEvent(EVENTS.MissionEnd)
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.MissionEnd
self:_Log("DCS MissionEnd event forwarding enabled")
end
if #self.DcsRegisteredEvents == 0 then
self:_Log("No supported DCS events available for forwarding")
return self
end
self.DcsEventForwardingStarted = true
return self
end
--- Unsubscribe from DCS events owned by this bridge instance.
function MOOSE_BRIDGE:_StopDcsEventForwarding()
if self.DcsEventForwardingStarted and self.UnHandleEvent then
for _, event_id in ipairs(self.DcsRegisteredEvents or {}) do
self:UnHandleEvent(event_id)
end
end
self.DcsRegisteredEvents = {}
self.DcsEventForwardingStarted = false
return self
end
--- Forward DCS S_EVENT_BASE_CAPTURED as airbase.coalition_changed.
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
function MOOSE_BRIDGE:OnEventBaseCaptured(EventData)
local ok, err = pcall(function()
local airbase, airbase_name = self:_AirbaseFromCapturedEvent(EventData)
if not airbase or not airbase_name then
error("BaseCaptured event has no resolvable AIRBASE")
end
local item = self:_BuildAirbaseSnapshotItem(airbase_name, airbase)
if not item or not item.object_id then
error("Could not build AIRBASE snapshot for " .. tostring(airbase_name))
end
self.AirbaseCoalitions = self.AirbaseCoalitions or {}
local previous = self.AirbaseCoalitions[item.object_id]
local current = item.coalition
self.AirbaseCoalitions[item.object_id] = current
if previous ~= current then
self:SendEvent("airbase.coalition_changed", {
dcs_event_id=EventData.id,
dcs_event_name="S_EVENT_BASE_CAPTURED",
dcs_event_time=EventData.time,
airbase_id=item.object_id,
previous_coalition=previous,
coalition=current,
capturing_unit_id=EventData.IniUnitName and ("UNIT:" .. tostring(EventData.IniUnitName)) or nil,
capturing_group_id=EventData.IniGroupName and ("GROUP:" .. tostring(EventData.IniGroupName)) or nil,
capturing_coalition=self:_CoalitionToName(EventData.IniCoalition),
capturing_unit_type=EventData.IniTypeName and tostring(EventData.IniTypeName) or nil,
airbase=item,
})
end
end)
if not ok then
self:_Log("Failed to forward BaseCaptured event: " .. tostring(err))
end
end
--- Build a tombstone and current group snapshot for a destroyed DCS object.
function MOOSE_BRIDGE:_BuildObjectDestroyedPayload(EventData)
if type(EventData) ~= "table" then error("Destruction event data is missing") end
local name = EventData.IniUnitName or EventData.IniDCSUnitName
if not name then error("Destruction event has no initiator name") end
local is_static = Object and Object.Category
and EventData.IniObjectCategory == Object.Category.STATIC
local is_scenery = Object and Object.Category
and EventData.IniObjectCategory == Object.Category.SCENERY
local object_type = is_scenery and "SCENERY" or (is_static and "STATIC" or "UNIT")
local object_id = object_type .. ":" .. tostring(name)
local item = nil
if is_scenery then
local scenery = EventData.IniDCSUnit or EventData.initiator
if scenery then
local ok, value = pcall(function()
return self:_ScenerySnapshot(
scenery,
nil,
name,
EventData.IniTypeName,
"destruction_event"
)
end)
if ok then item = value end
end
elseif is_static then
local static = EventData.IniUnit
if not static and _DATABASE and _DATABASE.STATICS then static = _DATABASE.STATICS[name] end
if static then
local ok, value = pcall(function() return self:_BuildStaticSnapshotItem(name, static) end)
if ok then item = value end
end
else
local unit = EventData.IniUnit
if not unit and _DATABASE and _DATABASE.UNITS then unit = _DATABASE.UNITS[name] end
if unit then
local ok, value = pcall(function() return self:_BuildUnitSnapshotItem(name, unit) end)
if ok then item = value end
end
end
item = item or {
object_id=object_id,
dcs_name=tostring(name),
object_type=object_type,
}
item.object_id = object_id
item.object_type = object_type
item.alive = false
item.active = false
if is_scenery then item.life = 0 end
item.coalition = item.coalition or self:_CoalitionToName(EventData.IniCoalition)
item.category = item.category or (is_scenery and "Scenery"
or (EventData.IniCategory and tostring(EventData.IniCategory) or nil))
item.dcs_type = item.dcs_type or item.type_name
or (EventData.IniTypeName and tostring(EventData.IniTypeName) or nil)
local group_name = EventData.IniGroupName or EventData.IniDCSGroupName or item.group_name
local group_item = nil
if not is_static and not is_scenery and group_name then
local group = EventData.IniGroup
if not group and _DATABASE and _DATABASE.GROUPS then group = _DATABASE.GROUPS[group_name] end
if group then
local ok, value = pcall(function() return self:_BuildGroupSnapshotItem(group_name, group) end)
if ok then group_item = value end
end
item.group_name = tostring(group_name)
end
return {
object_id=object_id,
object_type=object_type,
group_id=group_name and ("GROUP:" .. tostring(group_name)) or nil,
object=item,
group=group_item,
}
end
--- Forward one DCS destruction event as object.destroyed.
-- UnitLost and Dead can describe the same loss, depending on the DCS object
-- and destruction path. Suppress the second event without polling object state.
function MOOSE_BRIDGE:_ForwardObjectDestroyed(EventData, event_name)
local ok, err = pcall(function()
local payload = self:_BuildObjectDestroyedPayload(EventData)
local event_time = tonumber(EventData.time)
local dedup_time = event_time or (timer and timer.getTime and timer.getTime()) or 0
self.DcsDestroyedEventTimes = self.DcsDestroyedEventTimes or {}
local previous_time = self.DcsDestroyedEventTimes[payload.object_id]
if previous_time and math.abs(dedup_time - previous_time) <= 2 then return end
self.DcsDestroyedEventTimes[payload.object_id] = dedup_time
payload.dcs_event_id = EventData.id
payload.dcs_event_name = event_name
payload.dcs_event_time = event_time
self:SendEvent("object.destroyed", payload)
end)
if not ok then
self:_Log("Failed to forward " .. tostring(event_name) .. " event: " .. tostring(err))
end
end
--- Forward DCS S_EVENT_UNIT_LOST as object.destroyed.
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
function MOOSE_BRIDGE:OnEventUnitLost(EventData)
self:_ForwardObjectDestroyed(EventData, "S_EVENT_UNIT_LOST")
end
--- Forward DCS S_EVENT_DEAD as object.destroyed.
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
function MOOSE_BRIDGE:OnEventDead(EventData)
self:_ForwardObjectDestroyed(EventData, "S_EVENT_DEAD")
end
--- Forward an attributed DCS kill without replacing UnitLost/Dead state events.
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
function MOOSE_BRIDGE:OnEventKill(EventData)
local ok, err = pcall(function()
if type(EventData) ~= "table" then error("Kill event data is missing") end
local killer_name = EventData.IniUnitName or EventData.IniDCSUnitName
local target_name = EventData.TgtUnitName or EventData.TgtDCSUnitName
if not killer_name or not target_name then error("Kill event has no killer or target name") end
local target_is_static = Object and Object.Category
and EventData.TgtObjectCategory == Object.Category.STATIC
self:SendEvent("combat.kill", {
dcs_event_id=EventData.id,
dcs_event_name="S_EVENT_KILL",
dcs_event_time=EventData.time,
killer_object_id="UNIT:" .. tostring(killer_name),
killer_group_id=EventData.IniGroupName and ("GROUP:" .. tostring(EventData.IniGroupName)) or nil,
killer_coalition=self:_CoalitionToName(EventData.IniCoalition),
killer_type=EventData.IniTypeName and tostring(EventData.IniTypeName) or nil,
target_object_id=(target_is_static and "STATIC:" or "UNIT:") .. tostring(target_name),
target_group_id=EventData.TgtGroupName and ("GROUP:" .. tostring(EventData.TgtGroupName)) or nil,
target_coalition=self:_CoalitionToName(EventData.TgtCoalition),
target_type=EventData.TgtTypeName and tostring(EventData.TgtTypeName) or nil,
weapon_name=EventData.WeaponName and tostring(EventData.WeaponName) or nil,
})
end)
if not ok then
self:_Log("Failed to forward Kill event: " .. tostring(err))
end
end
--- Forward DCS S_EVENT_MISSION_END as the authoritative Python session boundary.
-- Flush immediately because normal bridge scheduling stops with the mission.
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
function MOOSE_BRIDGE:OnEventMissionEnd(EventData)
local ok, err = pcall(function()
self:SendEvent("mission.ended", {
dcs_event_id=EventData and EventData.id or nil,
dcs_event_name="S_EVENT_MISSION_END",
dcs_event_time=EventData and EventData.time or nil,
reason="dcs_mission_end",
})
self:_FlushOutQueue()
end)
if not ok then
self:_Log("Failed to forward MissionEnd event: " .. tostring(err))
end
end