mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2026-08-09 18:27:47 +00:00
PyBridge
This commit is contained in:
@@ -138,4 +138,13 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Beacons.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Radios.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Towns.lua' )
|
||||
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeJson.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridge.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeSocketTuningExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeDcsEventsExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeAuftragExecutionExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeAuftragTraceExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeIntelExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgePayloadExtension.lua' )
|
||||
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Globals.lua' )
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
-- Optional AUFTRAG tracing and diagnostic extension for MOOSE Bridge.
|
||||
--
|
||||
-- Load after MooseBridge.lua and, when used together with execution commands,
|
||||
-- after MooseBridgeAuftragExecutionExtension.lua. This file only adds read-only
|
||||
-- tracing helpers and does not change AUFTRAG execution semantics.
|
||||
|
||||
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgeAuftragTraceExtension.lua") end
|
||||
|
||||
local function trace_safe_tostring(value)
|
||||
if value == nil then return "nil" end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function trace_split_object_id(object_id)
|
||||
if type(object_id) ~= "string" then return nil, nil end
|
||||
local prefix, name = string.match(object_id, "^([^:]+):(.+)$")
|
||||
if not prefix or not name then return nil, nil end
|
||||
return string.upper(prefix), name
|
||||
end
|
||||
|
||||
local function trace_append_unique(result, seen, value)
|
||||
if value == nil then return end
|
||||
local text = tostring(value)
|
||||
if text == "" or seen[text] then return end
|
||||
result[#result + 1] = text
|
||||
seen[text] = true
|
||||
end
|
||||
|
||||
local function trace_bool(value)
|
||||
if value == nil then return false end
|
||||
return value and true or false
|
||||
end
|
||||
|
||||
local function trace_table_count(value)
|
||||
if type(value) ~= "table" then return 0 end
|
||||
local count = 0
|
||||
for _, _ in pairs(value) do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceAuftragId(value)
|
||||
if type(value) ~= "table" then return nil end
|
||||
return self:_AuftragObjectId(value)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectAuftragIds(queue)
|
||||
local result = {}; local seen = {}
|
||||
if type(queue) ~= "table" then return result end
|
||||
for _, auftrag in pairs(queue) do
|
||||
trace_append_unique(result, seen, self:_TraceAuftragId(auftrag))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceQueueContainsAuftrag(queue, auftrag_id)
|
||||
if type(queue) ~= "table" then return false end
|
||||
for _, auftrag in pairs(queue) do
|
||||
if self:_TraceAuftragId(auftrag) == auftrag_id then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceFindAuftrag(auftrag_id)
|
||||
if type(auftrag_id) ~= "string" or auftrag_id == "" then return nil, nil end
|
||||
|
||||
if type(self.TrackedAuftraege) == "table" and type(self.TrackedAuftraege[auftrag_id]) == "table" then
|
||||
return self.TrackedAuftraege[auftrag_id], "bridge.tracked"
|
||||
end
|
||||
|
||||
if _DATABASE and type(_DATABASE.LEGIONS) == "table" then
|
||||
for _, legion in pairs(_DATABASE.LEGIONS) do
|
||||
local queues = {legion.missionqueue, legion.missions, legion.auftraege, legion.missionQueue}
|
||||
for _, queue in ipairs(queues) do
|
||||
if type(queue) == "table" then
|
||||
for _, auftrag in pairs(queue) do
|
||||
if self:_TraceAuftragId(auftrag) == auftrag_id then return auftrag, "legion.queue" end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- MOOSE stores all OPSGROUP specializations here despite the FLIGHTGROUPS name.
|
||||
if _DATABASE and type(_DATABASE.FLIGHTGROUPS) == "table" then
|
||||
for _, opsgroup in pairs(_DATABASE.FLIGHTGROUPS) do
|
||||
local current = opsgroup.currentmission or opsgroup.missioncurrent or opsgroup.currentMission
|
||||
if self:_TraceAuftragId(current) == auftrag_id then return current, "opsgroup.current" end
|
||||
local queues = {opsgroup.missionqueue, opsgroup.missions, opsgroup.auftraege, opsgroup.missionQueue}
|
||||
for _, queue in ipairs(queues) do
|
||||
if type(queue) == "table" then
|
||||
for _, auftrag in pairs(queue) do
|
||||
if self:_TraceAuftragId(auftrag) == auftrag_id then return auftrag, "opsgroup.queue" end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceLegionItem(legion_name, legion, auftrag_id, source)
|
||||
local item = self:_BuildLegionSnapshotItem(legion_name, legion, source)
|
||||
if type(item) ~= "table" then return nil end
|
||||
item.missionqueue_count = trace_table_count(legion and legion.missionqueue)
|
||||
item.missionqueue_contains_auftrag = self:_TraceQueueContainsAuftrag(legion and legion.missionqueue, auftrag_id)
|
||||
item.is_running = tostring(item.state or "") == "Running"
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCohortItem(cohort_name, cohort, source)
|
||||
local item = self:_BuildCohortSnapshotItem(cohort_name, cohort, source)
|
||||
if type(item) ~= "table" then return nil end
|
||||
item.asset_count = item.asset_count or trace_table_count(cohort and cohort.assets)
|
||||
item.stock_asset_count = item.stock_asset_count or trace_table_count(cohort and cohort.stock)
|
||||
item.spawned_asset_count = item.spawned_asset_count or trace_table_count(cohort and cohort.spawnedassets)
|
||||
item.opsgroup_count = item.opsgroup_count or trace_table_count(cohort and cohort.opsgroups)
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceOpsGroupItem(opsgroup_name, opsgroup, auftrag_id, source)
|
||||
local item = self:_BuildOpsGroupSnapshotItem(opsgroup_name, opsgroup, source)
|
||||
if type(item) ~= "table" then return nil end
|
||||
item.current_contains_auftrag = item.auftrag_current_id == auftrag_id
|
||||
item.queue_contains_auftrag = self:_TraceQueueContainsAuftrag(opsgroup and opsgroup.missionqueue, auftrag_id)
|
||||
item.missionqueue_count = trace_table_count(opsgroup and opsgroup.missionqueue)
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectLegions(auftrag_id)
|
||||
local result = {}
|
||||
if _DATABASE and type(_DATABASE.LEGIONS) == "table" then
|
||||
for name, legion in pairs(_DATABASE.LEGIONS) do
|
||||
local ok, item = pcall(function() return self:_TraceLegionItem(name, legion, auftrag_id, "database.LEGIONS") end)
|
||||
if ok and item then result[#result + 1] = item end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectCohorts()
|
||||
local result = {}; local seen = {}
|
||||
if _DATABASE and type(_DATABASE.LEGIONS) == "table" then
|
||||
for _, legion in pairs(_DATABASE.LEGIONS) do
|
||||
if type(legion.cohorts) == "table" then
|
||||
for name, cohort in pairs(legion.cohorts) do
|
||||
local ok, item = pcall(function() return self:_TraceCohortItem(name, cohort, "legion.cohorts") end)
|
||||
if ok and item and item.object_id and not seen[item.object_id] then
|
||||
result[#result + 1] = item
|
||||
seen[item.object_id] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectOpsGroups(auftrag_id)
|
||||
local result = {}; local seen = {}
|
||||
for name, opsgroup in pairs(self.RegisteredOpsGroups or {}) do
|
||||
local ok, item = pcall(function() return self:_TraceOpsGroupItem(name, opsgroup, auftrag_id, "registered") end)
|
||||
if ok and item and item.object_id and not seen[item.object_id] then
|
||||
result[#result + 1] = item
|
||||
seen[item.object_id] = true
|
||||
end
|
||||
end
|
||||
-- MOOSE stores all OPSGROUP specializations here despite the FLIGHTGROUPS name.
|
||||
if _DATABASE and type(_DATABASE.FLIGHTGROUPS) == "table" then
|
||||
for name, opsgroup in pairs(_DATABASE.FLIGHTGROUPS) do
|
||||
local ok, item = pcall(function() return self:_TraceOpsGroupItem(name, opsgroup, auftrag_id, "database.FLIGHTGROUPS") end)
|
||||
if ok and item and item.object_id and not seen[item.object_id] then
|
||||
result[#result + 1] = item
|
||||
seen[item.object_id] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceBuild(auftrag_id)
|
||||
local auftrag, source = self:_TraceFindAuftrag(auftrag_id)
|
||||
local auftrag_item = nil
|
||||
if type(auftrag) == "table" then
|
||||
local ok, item = pcall(function() return self:_BuildAuftragSnapshotItem(auftrag, source or "trace") end)
|
||||
if ok then auftrag_item = item end
|
||||
end
|
||||
|
||||
local legions = self:_TraceCollectLegions(auftrag_id)
|
||||
local cohorts = self:_TraceCollectCohorts()
|
||||
local opsgroups = self:_TraceCollectOpsGroups(auftrag_id)
|
||||
|
||||
local matching_legions = {}
|
||||
for _, legion in ipairs(legions) do
|
||||
if legion.missionqueue_contains_auftrag then matching_legions[#matching_legions + 1] = legion.object_id end
|
||||
end
|
||||
|
||||
local matching_opsgroups = {}
|
||||
for _, opsgroup in ipairs(opsgroups) do
|
||||
if opsgroup.current_contains_auftrag or opsgroup.queue_contains_auftrag then matching_opsgroups[#matching_opsgroups + 1] = opsgroup.object_id end
|
||||
end
|
||||
|
||||
return {
|
||||
action="auftrag.trace",
|
||||
auftrag_id=auftrag_id,
|
||||
found=auftrag_item ~= nil,
|
||||
source=source,
|
||||
auftrag=auftrag_item,
|
||||
legions=legions,
|
||||
cohorts=cohorts,
|
||||
opsgroups=opsgroups,
|
||||
matching_legion_ids=matching_legions,
|
||||
matching_opsgroup_ids=matching_opsgroups,
|
||||
counts={
|
||||
legions=#legions,
|
||||
cohorts=#cohorts,
|
||||
opsgroups=#opsgroups,
|
||||
matching_legions=#matching_legions,
|
||||
matching_opsgroups=#matching_opsgroups,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:RegisterAuftragTraceCommands()
|
||||
self:RegisterCommand("auftrag.trace", function(cmd)
|
||||
local p = self:_CommandParams(cmd)
|
||||
local auftrag_id = p.auftrag_id or p.object_id or p.id
|
||||
if type(auftrag_id) ~= "string" or auftrag_id == "" then error("auftrag.trace requires auftrag_id") end
|
||||
local prefix, _ = trace_split_object_id(auftrag_id)
|
||||
if prefix ~= "AUFTRAG" then error("auftrag.trace requires an AUFTRAG:<id> object id") end
|
||||
return self:_TraceBuild(auftrag_id)
|
||||
end)
|
||||
end
|
||||
|
||||
local _moose_bridge_base_register_default_commands_for_trace = MOOSE_BRIDGE.RegisterDefaultCommands
|
||||
|
||||
function MOOSE_BRIDGE:RegisterDefaultCommands()
|
||||
_moose_bridge_base_register_default_commands_for_trace(self)
|
||||
self:RegisterAuftragTraceCommands()
|
||||
end
|
||||
@@ -0,0 +1,297 @@
|
||||
--- 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 lost DCS object.
|
||||
function MOOSE_BRIDGE:_BuildUnitLostPayload(EventData)
|
||||
if type(EventData) ~= "table" then error("UnitLost event data is missing") end
|
||||
local name = EventData.IniUnitName or EventData.IniDCSUnitName
|
||||
if not name then error("UnitLost event has no initiator name") end
|
||||
|
||||
local is_static = Object and Object.Category
|
||||
and EventData.IniObjectCategory == Object.Category.STATIC
|
||||
local object_type = is_static and "STATIC" or "UNIT"
|
||||
local object_id = object_type .. ":" .. tostring(name)
|
||||
local item = nil
|
||||
|
||||
if 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
|
||||
item.coalition = item.coalition or self:_CoalitionToName(EventData.IniCoalition)
|
||||
item.category = item.category or (EventData.IniCategory and tostring(EventData.IniCategory) or nil)
|
||||
item.dcs_type = item.dcs_type 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 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:_BuildUnitLostPayload(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
|
||||
@@ -0,0 +1,404 @@
|
||||
-- Optional INTEL snapshot and event extension for MOOSE Bridge.
|
||||
--
|
||||
-- MOOSE remains the owner of INTEL tactical logic. This extension only mirrors
|
||||
-- registered INTEL objects and forwards their FSM events to Python.
|
||||
|
||||
local function bridge_intel_safe_tostring(value)
|
||||
if value == nil then return nil end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function bridge_intel_object_name(object)
|
||||
if not object then return nil end
|
||||
if MOOSE_BRIDGE and MOOSE_BRIDGE._ObjectName then
|
||||
local ok, value = pcall(function() return MOOSE_BRIDGE:_ObjectName(object) end)
|
||||
if ok and value then return value end
|
||||
end
|
||||
if object.alias then return tostring(object.alias) end
|
||||
if object.name then return tostring(object.name) end
|
||||
if object.Name then return tostring(object.Name) end
|
||||
if object.groupname then return tostring(object.groupname) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function bridge_intel_auftrag_id(bridge, auftrag)
|
||||
if not auftrag then return nil end
|
||||
if bridge and bridge._AuftragObjectId then
|
||||
local ok, value = pcall(function() return bridge:_AuftragObjectId(auftrag) end)
|
||||
if ok and value then return value end
|
||||
end
|
||||
if auftrag.auftragsnummer then return "AUFTRAG:" .. tostring(auftrag.auftragsnummer) end
|
||||
if auftrag.name then return "AUFTRAG:" .. tostring(auftrag.name) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function bridge_intel_point(bridge, value)
|
||||
if not value then return nil end
|
||||
if bridge and bridge._PointFromMooseObject then
|
||||
local ok, point = pcall(function() return bridge:_PointFromMooseObject(value) end)
|
||||
if ok and point then return point end
|
||||
end
|
||||
if type(value) == "table" and type(value.x) == "number" and type(value.z) == "number" then
|
||||
return value
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function bridge_intel_velocity(value)
|
||||
if type(value) ~= "table" then return nil end
|
||||
return {
|
||||
x=type(value.x) == "number" and value.x or nil,
|
||||
y=type(value.y) == "number" and value.y or nil,
|
||||
z=type(value.z) == "number" and value.z or nil,
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_EnsureIntelRegistry()
|
||||
self.RegisteredIntels = self.RegisteredIntels or {}
|
||||
return self.RegisteredIntels
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:RegisterIntel(intel, name)
|
||||
if not intel then return self end
|
||||
local intel_name = name or bridge_intel_object_name(intel) or intel.alias
|
||||
if not intel_name then return self end
|
||||
intel_name = tostring(intel_name)
|
||||
self:_EnsureIntelRegistry()[intel_name] = intel
|
||||
self:_AttachIntelEventForwarders(intel, intel_name)
|
||||
if type(intel.SetAgentAuto) ~= "function" then error("INTEL:SetAgentAuto is not available") end
|
||||
intel:SetAgentAuto()
|
||||
return self
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:RegisterIntels(intels)
|
||||
if type(intels) ~= "table" then return self end
|
||||
for name, intel in pairs(intels) do self:RegisterIntel(intel, name) end
|
||||
return self
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelObjectId(intel_name)
|
||||
return "INTEL:" .. tostring(intel_name)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelContactId(intel_name, contact)
|
||||
local name = contact and contact.groupname or nil
|
||||
if not name then return nil end
|
||||
return "INTELCONTACT:" .. tostring(intel_name) .. ":" .. tostring(name)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelClusterId(intel_name, cluster)
|
||||
local index = cluster and cluster.index or nil
|
||||
if not index then return nil end
|
||||
return "INTELCLUSTER:" .. tostring(intel_name) .. ":" .. tostring(index)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelContactTargetObjectId(contact)
|
||||
if not contact then return nil end
|
||||
local name = contact.groupname
|
||||
if not name then return nil end
|
||||
if contact.isStatic then return "STATIC:" .. tostring(name) end
|
||||
return "GROUP:" .. tostring(name)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelAgentCounts(intel)
|
||||
local detectionset = intel and intel.detectionset or nil
|
||||
if not detectionset then return 0, 0 end
|
||||
return detectionset:Count(), detectionset:CountAlive()
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelAgentSnapshot(intel)
|
||||
local result = {}
|
||||
local detectionset = intel and intel.detectionset or nil
|
||||
if not detectionset then return result, 0, 0 end
|
||||
|
||||
for name, _ in pairs(detectionset.Set or {}) do
|
||||
result[#result + 1] = "GROUP:" .. tostring(name)
|
||||
end
|
||||
|
||||
local agent_count, alive_agent_count = self:_IntelAgentCounts(intel)
|
||||
return result, agent_count, alive_agent_count
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_ResolveRegisteredIntel(intel_id)
|
||||
local name = type(intel_id) == "string" and string.match(intel_id, "^INTEL:(.+)$") or nil
|
||||
if not name then return nil, nil, "Invalid INTEL object id: " .. tostring(intel_id) end
|
||||
local intel = self:_EnsureIntelRegistry()[name]
|
||||
if not intel then return nil, nil, "INTEL not registered: " .. name end
|
||||
return intel, name, nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_ResolveIntelAgent(agent_id)
|
||||
local prefix, name
|
||||
if type(agent_id) == "string" then prefix, name = string.match(agent_id, "^([^:]+):(.+)$") end
|
||||
if not prefix or not name then return nil, "Invalid agent object id: " .. tostring(agent_id) end
|
||||
|
||||
if prefix == "GROUP" then
|
||||
local group = GROUP and GROUP.FindByName and GROUP:FindByName(name) or nil
|
||||
if not group then return nil, "GROUP not found: " .. name end
|
||||
return group, nil
|
||||
end
|
||||
|
||||
if prefix == "OPSGROUP" then
|
||||
if not self._ResolveOpsGroupById then
|
||||
return nil, "OPSGROUP agents require MooseBridgeAuftragExecutionExtension.lua"
|
||||
end
|
||||
return self:_ResolveOpsGroupById(agent_id)
|
||||
end
|
||||
|
||||
return nil, "Agent must be GROUP:<name> or OPSGROUP:<name>"
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_BuildIntelContactSnapshotItem(intel_name, contact, source)
|
||||
if type(contact) ~= "table" then return nil end
|
||||
local point = bridge_intel_point(self, contact.position)
|
||||
local recce_name = bridge_intel_safe_tostring(contact.recce)
|
||||
local recce_unit = recce_name and UNIT and UNIT.FindByName and UNIT:FindByName(recce_name) or nil
|
||||
local recce_group = recce_unit and self:_SafeCall(recce_unit, "GetGroup") or nil
|
||||
local recce_group_name = recce_group and self:_SafeCall(recce_group, "GetName") or nil
|
||||
local item = {
|
||||
object_id=self:_IntelContactId(intel_name, contact),
|
||||
dcs_name=bridge_intel_safe_tostring(contact.groupname),
|
||||
object_type="INTELCONTACT",
|
||||
category=bridge_intel_safe_tostring(contact.ctype or contact.categoryname),
|
||||
source=source,
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
target_object_id=self:_IntelContactTargetObjectId(contact),
|
||||
typename=bridge_intel_safe_tostring(contact.typename),
|
||||
attribute=bridge_intel_safe_tostring(contact.attribute),
|
||||
category_id=contact.category,
|
||||
category_name=bridge_intel_safe_tostring(contact.categoryname),
|
||||
threat_level=contact.threatlevel,
|
||||
detected_time=contact.Tdetected,
|
||||
recce=recce_name,
|
||||
recce_unit_id=recce_name and "UNIT:" .. recce_name or nil,
|
||||
recce_group_id=recce_group_name and "GROUP:" .. tostring(recce_group_name) or nil,
|
||||
contact_type=bridge_intel_safe_tostring(contact.ctype),
|
||||
speed_mps=contact.speed,
|
||||
velocity=bridge_intel_velocity(contact.velocity),
|
||||
is_ground=contact.isground and true or false,
|
||||
is_ship=contact.isship and true or false,
|
||||
is_static=contact.isStatic and true or false,
|
||||
platform=bridge_intel_safe_tostring(contact.platform),
|
||||
heading=contact.heading,
|
||||
maneuvering=contact.maneuvering and true or false,
|
||||
altitude_m=contact.altitude,
|
||||
rcs=contact.rcs,
|
||||
mission_id=bridge_intel_auftrag_id(self, contact.mission),
|
||||
}
|
||||
if point then self:_AddPointFields(item, point) end
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_BuildIntelClusterSnapshotItem(intel_name, cluster, source)
|
||||
if type(cluster) ~= "table" then return nil end
|
||||
local point = bridge_intel_point(self, cluster.coordinate)
|
||||
local contact_ids = {}
|
||||
if type(cluster.Contacts) == "table" then
|
||||
for _, contact in pairs(cluster.Contacts) do
|
||||
local contact_id = self:_IntelContactId(intel_name, contact)
|
||||
if contact_id then contact_ids[#contact_ids + 1] = contact_id end
|
||||
end
|
||||
end
|
||||
local item = {
|
||||
object_id=self:_IntelClusterId(intel_name, cluster),
|
||||
dcs_name="Cluster " .. tostring(cluster.index or "?"),
|
||||
object_type="INTELCLUSTER",
|
||||
category=bridge_intel_safe_tostring(cluster.ctype),
|
||||
source=source,
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
index=cluster.index,
|
||||
size=cluster.size,
|
||||
contact_ids=contact_ids,
|
||||
threat_level_max=cluster.threatlevelMax,
|
||||
threat_level_sum=cluster.threatlevelSum,
|
||||
threat_level_avg=cluster.threatlevelAve,
|
||||
contact_type=bridge_intel_safe_tostring(cluster.ctype),
|
||||
altitude_m=cluster.altitude,
|
||||
mission_id=bridge_intel_auftrag_id(self, cluster.mission),
|
||||
}
|
||||
if point then self:_AddPointFields(item, point) end
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_BuildIntelSnapshotItem(intel_name, intel, source)
|
||||
local contacts = self:_SafeCall(intel, "GetContactTable") or intel.Contacts or {}
|
||||
local clusters = self:_SafeCall(intel, "GetClusterTable") or intel.Clusters or {}
|
||||
local agent_ids, agent_count, alive_agent_count = self:_IntelAgentSnapshot(intel)
|
||||
return {
|
||||
object_id=self:_IntelObjectId(intel_name),
|
||||
dcs_name=tostring(intel_name),
|
||||
object_type="INTEL",
|
||||
category="INTEL",
|
||||
source=source,
|
||||
alias=bridge_intel_safe_tostring(intel.alias),
|
||||
coalition=self:_CoalitionToName(intel.coalition),
|
||||
state=self:_SafeCallArg(intel, "GetState") or nil,
|
||||
is_running=self:_SafeCallArg(intel, "Is", "Running") and true or false,
|
||||
cluster_analysis=intel.clusteranalysis and true or false,
|
||||
cluster_markers=intel.clustermarkers and true or false,
|
||||
cluster_arrows=intel.clusterarrows and true or false,
|
||||
cluster_radius_m=intel.clusterradius,
|
||||
detect_statics=intel.detectStatics and true or false,
|
||||
detect_accoustic=intel.DetectAccoustic and true or false,
|
||||
detect_accoustic_radius_m=intel.DetectAccousticRadius,
|
||||
doppler_radar=intel.DopplerRadar and true or false,
|
||||
contact_count=type(contacts) == "table" and #contacts or 0,
|
||||
cluster_count=type(clusters) == "table" and #clusters or 0,
|
||||
agent_count=agent_count,
|
||||
alive_agent_count=alive_agent_count,
|
||||
agent_ids=agent_ids,
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:BuildIntelSnapshot()
|
||||
local result = {}
|
||||
for name, intel in pairs(self:_EnsureIntelRegistry()) do
|
||||
local ok, item = pcall(function() return self:_BuildIntelSnapshotItem(name, intel, "registered") end)
|
||||
if ok and item then result[#result + 1] = item end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:BuildIntelContactSnapshot()
|
||||
local result = {}
|
||||
for name, intel in pairs(self:_EnsureIntelRegistry()) do
|
||||
local contacts = self:_SafeCall(intel, "GetContactTable") or intel.Contacts or {}
|
||||
if type(contacts) == "table" then
|
||||
for _, contact in pairs(contacts) do
|
||||
local ok, item = pcall(function() return self:_BuildIntelContactSnapshotItem(name, contact, "registered") end)
|
||||
if ok and item and item.object_id then result[#result + 1] = item end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:BuildIntelClusterSnapshot()
|
||||
local result = {}
|
||||
for name, intel in pairs(self:_EnsureIntelRegistry()) do
|
||||
local clusters = self:_SafeCall(intel, "GetClusterTable") or intel.Clusters or {}
|
||||
if type(clusters) == "table" then
|
||||
for _, cluster in pairs(clusters) do
|
||||
local ok, item = pcall(function() return self:_BuildIntelClusterSnapshotItem(name, cluster, "registered") end)
|
||||
if ok and item and item.object_id then result[#result + 1] = item end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_SendIntelEvent(event_name, intel_name, fsm_event, from_state, to_state, item_kind, item)
|
||||
local payload = {
|
||||
event=event_name,
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
fsm_event=fsm_event,
|
||||
from_state=from_state,
|
||||
to_state=to_state,
|
||||
}
|
||||
if item_kind == "contact" then
|
||||
payload.contact = item
|
||||
payload.contact_id = item and item.object_id or nil
|
||||
payload.target_object_id = item and item.target_object_id or nil
|
||||
elseif item_kind == "cluster" then
|
||||
payload.cluster = item
|
||||
payload.cluster_id = item and item.object_id or nil
|
||||
end
|
||||
self:SendEvent(event_name, payload)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_AttachIntelEventForwarders(intel, intel_name)
|
||||
if type(intel) ~= "table" or intel.MooseBridgeIntelEventsRegistered then return self end
|
||||
intel.MooseBridgeIntelEventsRegistered = true
|
||||
local bridge = self
|
||||
local previous_new_contact = intel.OnAfterNewContact
|
||||
local previous_lost_contact = intel.OnAfterLostContact
|
||||
local previous_new_cluster = intel.OnAfterNewCluster
|
||||
local previous_lost_cluster = intel.OnAfterLostCluster
|
||||
|
||||
intel.OnAfterNewContact = function(intel_self, From, Event, To, Contact)
|
||||
if type(previous_new_contact) == "function" then pcall(previous_new_contact, intel_self, From, Event, To, Contact) end
|
||||
local item = bridge:_BuildIntelContactSnapshotItem(intel_name, Contact, "event")
|
||||
bridge:_SendIntelEvent("intel.new_contact", intel_name, Event, From, To, "contact", item)
|
||||
end
|
||||
|
||||
intel.OnAfterLostContact = function(intel_self, From, Event, To, Contact)
|
||||
if type(previous_lost_contact) == "function" then pcall(previous_lost_contact, intel_self, From, Event, To, Contact) end
|
||||
local item = bridge:_BuildIntelContactSnapshotItem(intel_name, Contact, "event")
|
||||
bridge:_SendIntelEvent("intel.lost_contact", intel_name, Event, From, To, "contact", item)
|
||||
end
|
||||
|
||||
intel.OnAfterNewCluster = function(intel_self, From, Event, To, Cluster)
|
||||
if type(previous_new_cluster) == "function" then pcall(previous_new_cluster, intel_self, From, Event, To, Cluster) end
|
||||
local item = bridge:_BuildIntelClusterSnapshotItem(intel_name, Cluster, "event")
|
||||
bridge:_SendIntelEvent("intel.new_cluster", intel_name, Event, From, To, "cluster", item)
|
||||
end
|
||||
|
||||
intel.OnAfterLostCluster = function(intel_self, From, Event, To, Cluster, Mission)
|
||||
if type(previous_lost_cluster) == "function" then pcall(previous_lost_cluster, intel_self, From, Event, To, Cluster, Mission) end
|
||||
local item = bridge:_BuildIntelClusterSnapshotItem(intel_name, Cluster, "event")
|
||||
if item and not item.mission_id then item.mission_id = bridge_intel_auftrag_id(bridge, Mission) end
|
||||
bridge:_SendIntelEvent("intel.lost_cluster", intel_name, Event, From, To, "cluster", item)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
local _moose_bridge_base_register_default_commands_for_intel = MOOSE_BRIDGE.RegisterDefaultCommands
|
||||
|
||||
function MOOSE_BRIDGE:RegisterDefaultCommands()
|
||||
_moose_bridge_base_register_default_commands_for_intel(self)
|
||||
|
||||
local previous_snapshot_all = self.CommandHandlers["snapshot.all"]
|
||||
|
||||
self:RegisterCommand("snapshot.intels", function(cmd)
|
||||
local intels = self:BuildIntelSnapshot()
|
||||
self:SendSnapshot("intels", {intels=intels})
|
||||
return {kind="intels", count=#intels}
|
||||
end)
|
||||
|
||||
self:RegisterCommand("intel.add_agent", function(cmd)
|
||||
local params = self:_CommandParams(cmd)
|
||||
local intel, intel_name, intel_err = self:_ResolveRegisteredIntel(params.intel_id)
|
||||
if not intel then error(intel_err) end
|
||||
local agent, agent_err = self:_ResolveIntelAgent(params.agent_id)
|
||||
if not agent then error(agent_err) end
|
||||
|
||||
intel:AddAgent(agent)
|
||||
local agent_count, alive_agent_count = self:_IntelAgentCounts(intel)
|
||||
return {
|
||||
action="intel.add_agent",
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
agent_id=params.agent_id,
|
||||
agent_count=agent_count,
|
||||
alive_agent_count=alive_agent_count,
|
||||
}
|
||||
end)
|
||||
|
||||
self:RegisterCommand("snapshot.intel_contacts", function(cmd)
|
||||
local contacts = self:BuildIntelContactSnapshot()
|
||||
self:SendSnapshot("intel_contacts", {intel_contacts=contacts})
|
||||
return {kind="intel_contacts", count=#contacts}
|
||||
end)
|
||||
|
||||
self:RegisterCommand("snapshot.intel_clusters", function(cmd)
|
||||
local clusters = self:BuildIntelClusterSnapshot()
|
||||
self:SendSnapshot("intel_clusters", {intel_clusters=clusters})
|
||||
return {kind="intel_clusters", count=#clusters}
|
||||
end)
|
||||
|
||||
if previous_snapshot_all then
|
||||
self:RegisterCommand("snapshot.all", function(cmd)
|
||||
local result = previous_snapshot_all(cmd) or {}
|
||||
local intels = self:BuildIntelSnapshot()
|
||||
local contacts = self:BuildIntelContactSnapshot()
|
||||
local clusters = self:BuildIntelClusterSnapshot()
|
||||
self:SendSnapshot("intels", {intels=intels})
|
||||
self:SendSnapshot("intel_contacts", {intel_contacts=contacts})
|
||||
self:SendSnapshot("intel_clusters", {intel_clusters=clusters})
|
||||
result.intels = #intels
|
||||
result.intel_contacts = #contacts
|
||||
result.intel_clusters = #clusters
|
||||
return result
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,258 @@
|
||||
--- Minimal JSON helper for the MOOSE Bridge V1 prototype.
|
||||
-- This is deliberately small and covers the V1 command/ack/heartbeat/snapshot payloads.
|
||||
|
||||
MOOSE_BRIDGE_JSON = MOOSE_BRIDGE_JSON or {}
|
||||
local json = MOOSE_BRIDGE_JSON
|
||||
|
||||
local function escape(value)
|
||||
value = tostring(value or "")
|
||||
value = value:gsub('\\', '\\\\')
|
||||
value = value:gsub('"', '\\"')
|
||||
value = value:gsub('\n', '\\n')
|
||||
value = value:gsub('\r', '\\r')
|
||||
value = value:gsub('\t', '\\t')
|
||||
return value
|
||||
end
|
||||
|
||||
local function is_array(value)
|
||||
if type(value) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local max_index = 0
|
||||
local count = 0
|
||||
|
||||
for key, _ in pairs(value) do
|
||||
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
|
||||
return false
|
||||
end
|
||||
if key > max_index then
|
||||
max_index = key
|
||||
end
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
return count == max_index
|
||||
end
|
||||
|
||||
local function encode_value(value)
|
||||
local t = type(value)
|
||||
|
||||
if value == nil then
|
||||
return "null"
|
||||
elseif t == "boolean" then
|
||||
return value and "true" or "false"
|
||||
elseif t == "number" then
|
||||
return tostring(value)
|
||||
elseif t == "string" then
|
||||
return '"' .. escape(value) .. '"'
|
||||
elseif t == "table" then
|
||||
local parts = {}
|
||||
|
||||
if is_array(value) then
|
||||
for index = 1, #value do
|
||||
parts[#parts + 1] = encode_value(value[index])
|
||||
end
|
||||
return "[" .. table.concat(parts, ",") .. "]"
|
||||
end
|
||||
|
||||
for k, v in pairs(value) do
|
||||
parts[#parts + 1] = '"' .. escape(k) .. '":' .. encode_value(v)
|
||||
end
|
||||
return "{" .. table.concat(parts, ",") .. "}"
|
||||
end
|
||||
|
||||
return '"' .. escape(value) .. '"'
|
||||
end
|
||||
|
||||
function json.encode(value)
|
||||
return encode_value(value)
|
||||
end
|
||||
|
||||
local function decode_error(text, index, message)
|
||||
error("JSON decode error at byte " .. tostring(index) .. ": " .. message .. " near " .. string.format("%q", text:sub(index, index + 20)))
|
||||
end
|
||||
|
||||
local function skip_ws(text, index)
|
||||
while index <= #text do
|
||||
local char = text:sub(index, index)
|
||||
if char ~= " " and char ~= "\n" and char ~= "\r" and char ~= "\t" then break end
|
||||
index = index + 1
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
local function utf8_char(codepoint)
|
||||
if codepoint <= 0x7F then
|
||||
return string.char(codepoint)
|
||||
elseif codepoint <= 0x7FF then
|
||||
return string.char(
|
||||
0xC0 + math.floor(codepoint / 0x40),
|
||||
0x80 + (codepoint % 0x40)
|
||||
)
|
||||
elseif codepoint <= 0xFFFF then
|
||||
return string.char(
|
||||
0xE0 + math.floor(codepoint / 0x1000),
|
||||
0x80 + (math.floor(codepoint / 0x40) % 0x40),
|
||||
0x80 + (codepoint % 0x40)
|
||||
)
|
||||
elseif codepoint <= 0x10FFFF then
|
||||
return string.char(
|
||||
0xF0 + math.floor(codepoint / 0x40000),
|
||||
0x80 + (math.floor(codepoint / 0x1000) % 0x40),
|
||||
0x80 + (math.floor(codepoint / 0x40) % 0x40),
|
||||
0x80 + (codepoint % 0x40)
|
||||
)
|
||||
end
|
||||
return "?"
|
||||
end
|
||||
|
||||
local parse_value
|
||||
|
||||
local function parse_string(text, index)
|
||||
if text:sub(index, index) ~= '"' then decode_error(text, index, "expected string") end
|
||||
index = index + 1
|
||||
local parts = {}
|
||||
local start = index
|
||||
|
||||
while index <= #text do
|
||||
local char = text:sub(index, index)
|
||||
if char == '"' then
|
||||
parts[#parts + 1] = text:sub(start, index - 1)
|
||||
return table.concat(parts), index + 1
|
||||
elseif char == "\\" then
|
||||
parts[#parts + 1] = text:sub(start, index - 1)
|
||||
local escape_char = text:sub(index + 1, index + 1)
|
||||
if escape_char == '"' or escape_char == "\\" or escape_char == "/" then
|
||||
parts[#parts + 1] = escape_char
|
||||
index = index + 2
|
||||
elseif escape_char == "b" then
|
||||
parts[#parts + 1] = "\b"
|
||||
index = index + 2
|
||||
elseif escape_char == "f" then
|
||||
parts[#parts + 1] = "\f"
|
||||
index = index + 2
|
||||
elseif escape_char == "n" then
|
||||
parts[#parts + 1] = "\n"
|
||||
index = index + 2
|
||||
elseif escape_char == "r" then
|
||||
parts[#parts + 1] = "\r"
|
||||
index = index + 2
|
||||
elseif escape_char == "t" then
|
||||
parts[#parts + 1] = "\t"
|
||||
index = index + 2
|
||||
elseif escape_char == "u" then
|
||||
local hex = text:sub(index + 2, index + 5)
|
||||
local codepoint = tonumber(hex, 16)
|
||||
if not codepoint then decode_error(text, index, "invalid unicode escape") end
|
||||
index = index + 6
|
||||
|
||||
if codepoint >= 0xD800 and codepoint <= 0xDBFF and text:sub(index, index + 1) == "\\u" then
|
||||
local low = tonumber(text:sub(index + 2, index + 5), 16)
|
||||
if low and low >= 0xDC00 and low <= 0xDFFF then
|
||||
codepoint = 0x10000 + ((codepoint - 0xD800) * 0x400) + (low - 0xDC00)
|
||||
index = index + 6
|
||||
end
|
||||
end
|
||||
|
||||
parts[#parts + 1] = utf8_char(codepoint)
|
||||
else
|
||||
decode_error(text, index, "invalid escape sequence")
|
||||
end
|
||||
start = index
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
|
||||
decode_error(text, index, "unterminated string")
|
||||
end
|
||||
|
||||
local function parse_number(text, index)
|
||||
local start = index
|
||||
if text:sub(index, index) == "-" then index = index + 1 end
|
||||
while text:sub(index, index):match("%d") do index = index + 1 end
|
||||
if text:sub(index, index) == "." then
|
||||
index = index + 1
|
||||
while text:sub(index, index):match("%d") do index = index + 1 end
|
||||
end
|
||||
local exponent = text:sub(index, index)
|
||||
if exponent == "e" or exponent == "E" then
|
||||
index = index + 1
|
||||
local sign = text:sub(index, index)
|
||||
if sign == "+" or sign == "-" then index = index + 1 end
|
||||
while text:sub(index, index):match("%d") do index = index + 1 end
|
||||
end
|
||||
local raw = text:sub(start, index - 1)
|
||||
local value = tonumber(raw)
|
||||
if value == nil then decode_error(text, start, "invalid number") end
|
||||
return value, index
|
||||
end
|
||||
|
||||
local function parse_array(text, index)
|
||||
index = skip_ws(text, index + 1)
|
||||
local result = {}
|
||||
if text:sub(index, index) == "]" then return result, index + 1 end
|
||||
|
||||
while index <= #text do
|
||||
local value
|
||||
value, index = parse_value(text, index)
|
||||
result[#result + 1] = value
|
||||
index = skip_ws(text, index)
|
||||
local char = text:sub(index, index)
|
||||
if char == "]" then return result, index + 1 end
|
||||
if char ~= "," then decode_error(text, index, "expected ',' or ']'") end
|
||||
index = skip_ws(text, index + 1)
|
||||
end
|
||||
|
||||
decode_error(text, index, "unterminated array")
|
||||
end
|
||||
|
||||
local function parse_object(text, index)
|
||||
index = skip_ws(text, index + 1)
|
||||
local result = {}
|
||||
if text:sub(index, index) == "}" then return result, index + 1 end
|
||||
|
||||
while index <= #text do
|
||||
local key
|
||||
key, index = parse_string(text, index)
|
||||
index = skip_ws(text, index)
|
||||
if text:sub(index, index) ~= ":" then decode_error(text, index, "expected ':'") end
|
||||
index = skip_ws(text, index + 1)
|
||||
local value
|
||||
value, index = parse_value(text, index)
|
||||
result[key] = value
|
||||
index = skip_ws(text, index)
|
||||
local char = text:sub(index, index)
|
||||
if char == "}" then return result, index + 1 end
|
||||
if char ~= "," then decode_error(text, index, "expected ',' or '}'") end
|
||||
index = skip_ws(text, index + 1)
|
||||
end
|
||||
|
||||
decode_error(text, index, "unterminated object")
|
||||
end
|
||||
|
||||
parse_value = function(text, index)
|
||||
index = skip_ws(text, index)
|
||||
local char = text:sub(index, index)
|
||||
if char == '"' then return parse_string(text, index) end
|
||||
if char == "{" then return parse_object(text, index) end
|
||||
if char == "[" then return parse_array(text, index) end
|
||||
if char == "-" or char:match("%d") then return parse_number(text, index) end
|
||||
if text:sub(index, index + 3) == "true" then return true, index + 4 end
|
||||
if text:sub(index, index + 4) == "false" then return false, index + 5 end
|
||||
if text:sub(index, index + 3) == "null" then return nil, index + 4 end
|
||||
decode_error(text, index, "unexpected value")
|
||||
end
|
||||
|
||||
function json.decode(text)
|
||||
if type(text) ~= "string" then error("JSON decode expects a string") end
|
||||
local value, index = parse_value(text, 1)
|
||||
index = skip_ws(text, index)
|
||||
if index <= #text then decode_error(text, index, "trailing characters") end
|
||||
if type(value) ~= "table" then error("JSON command must decode to an object") end
|
||||
if type(value.params) ~= "table" then value.params = {} end
|
||||
return value
|
||||
end
|
||||
|
||||
return json
|
||||
@@ -0,0 +1,105 @@
|
||||
-- Optional AIRWING payload snapshot extension for MOOSE Bridge.
|
||||
--
|
||||
-- Load after MooseBridge.lua when AIRWING payload availability should be included
|
||||
-- in COHORT snapshots. The core Python advisory layer can then reject AIRWING
|
||||
-- candidates without a compatible payload for the requested AUFTRAG type.
|
||||
|
||||
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgePayloadExtension.lua") end
|
||||
|
||||
local function bridge_safe_tostring(value)
|
||||
if value == nil then return "nil" end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function bridge_string_or_nil(value)
|
||||
if value == nil then return nil end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_CohortUnitType(cohort)
|
||||
if not cohort then return nil end
|
||||
local unit_type = self:_SafeCall(cohort, "GetUnitType")
|
||||
if not unit_type then unit_type = self:_SafeCall(cohort, "GetTypeName") end
|
||||
if not unit_type then unit_type = self:_SafeCall(cohort, "GetType") end
|
||||
if not unit_type then unit_type = cohort.unittype or cohort.unitType or cohort.aircrafttype or cohort.AircraftType or cohort.type end
|
||||
return unit_type and bridge_safe_tostring(unit_type) or nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_PayloadPerformance(payload, mission_type)
|
||||
if type(payload) ~= "table" or type(payload.capabilities) ~= "table" then return nil end
|
||||
for _, capability in pairs(payload.capabilities) do
|
||||
if type(capability) == "table" and capability.MissionType == mission_type then
|
||||
return self:_NumberOrNil(capability.Performance)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_SummarizePayload(payload, mission_type)
|
||||
if type(payload) ~= "table" then return nil end
|
||||
local performance = self:_PayloadPerformance(payload, mission_type)
|
||||
return {
|
||||
uid=payload.uid,
|
||||
unitname=bridge_string_or_nil(payload.unitname),
|
||||
aircrafttype=bridge_string_or_nil(payload.aircrafttype),
|
||||
navail=self:_NumberOrNil(payload.navail),
|
||||
unlimited=self:_BoolOrFalse(payload.unlimited),
|
||||
performance=performance,
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_PayloadAvailabilityForMission(airwing, unit_type, mission_type)
|
||||
local payloads = self:_SafeCallArg(airwing, "_FilterPlayloads", unit_type, mission_type)
|
||||
if payloads == nil then payloads = self:_SafeCallArg(airwing, "_FilterPayloads", unit_type, mission_type) end
|
||||
|
||||
local result = {
|
||||
available_count=0,
|
||||
total_available=0,
|
||||
unlimited_count=0,
|
||||
best_performance=nil,
|
||||
payloads={},
|
||||
}
|
||||
|
||||
if type(payloads) ~= "table" then return result end
|
||||
|
||||
for _, payload in pairs(payloads) do
|
||||
local item = self:_SummarizePayload(payload, mission_type)
|
||||
if item then
|
||||
result.payloads[#result.payloads + 1] = item
|
||||
result.available_count = result.available_count + 1
|
||||
if item.unlimited then result.unlimited_count = result.unlimited_count + 1 end
|
||||
if item.navail then result.total_available = result.total_available + item.navail end
|
||||
if item.performance and (not result.best_performance or item.performance > result.best_performance) then
|
||||
result.best_performance = item.performance
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_CollectPayloadAvailability(cohort, mission_types)
|
||||
local result = {}
|
||||
if not cohort or type(mission_types) ~= "table" then return result end
|
||||
local airwing = cohort.legion
|
||||
if not airwing or not self:_SafeCall(airwing, "IsAirwing") then return result end
|
||||
local unit_type = self:_CohortUnitType(cohort)
|
||||
if not unit_type then return result end
|
||||
|
||||
for _, mission_type in pairs(mission_types) do
|
||||
result[bridge_safe_tostring(mission_type)] = self:_PayloadAvailabilityForMission(airwing, unit_type, mission_type)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local _moose_bridge_base_build_cohort_snapshot_item = MOOSE_BRIDGE._BuildCohortSnapshotItem
|
||||
|
||||
function MOOSE_BRIDGE:_BuildCohortSnapshotItem(cohort_name, cohort, source)
|
||||
local item = _moose_bridge_base_build_cohort_snapshot_item(self, cohort_name, cohort, source)
|
||||
if not item then return nil end
|
||||
local unit_type = self:_CohortUnitType(cohort)
|
||||
item.unit_type = unit_type
|
||||
item.payloads_by_mission = self:_CollectPayloadAvailability(cohort, item.mission_types)
|
||||
return item
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Optional connection tuning extension for MOOSE Bridge.
|
||||
--
|
||||
-- Load after MooseBridge.lua and before creating the bridge instance. It reduces
|
||||
-- DCS main-thread stalls when the Python server is not running by shortening the
|
||||
-- blocking LuaSocket connect timeout and using less aggressive retry defaults.
|
||||
|
||||
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgeSocketTuningExtension.lua") end
|
||||
|
||||
local function bridge_tuning_mission_time()
|
||||
if timer and timer.getTime then return timer.getTime() end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function bridge_tuning_safe_tostring(value)
|
||||
if value == nil then return "nil" end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function bridge_should_log_connect_failure(bridge, err, now)
|
||||
if bridge.LogConnectFailures == false then return false end
|
||||
if err == "timeout" and bridge.LogConnectTimeouts ~= true then return false end
|
||||
|
||||
local interval = bridge.ConnectFailureLogInterval or 60
|
||||
if bridge.LastConnectFailureLog and now - bridge.LastConnectFailureLog < interval then return false end
|
||||
|
||||
bridge.LastConnectFailureLog = now
|
||||
return true
|
||||
end
|
||||
|
||||
local _moose_bridge_tuning_base_new = MOOSE_BRIDGE.New
|
||||
|
||||
function MOOSE_BRIDGE:New(host, port)
|
||||
local bridge = _moose_bridge_tuning_base_new(self, host, port)
|
||||
|
||||
-- Keep idle bridge retries low-impact when the Python server is not listening.
|
||||
bridge.ConnectTimeout = bridge.ConnectTimeout or 0.02
|
||||
if bridge.ConnectRetryDelay == nil or bridge.ConnectRetryDelay == 5 then bridge.ConnectRetryDelay = 10 end
|
||||
if bridge.TickInterval == nil or bridge.TickInterval == 0.2 then bridge.TickInterval = 0.5 end
|
||||
if bridge.HeartbeatInterval == nil or bridge.HeartbeatInterval == 5 then bridge.HeartbeatInterval = 10 end
|
||||
|
||||
-- Keep expected idle reconnect timeouts out of dcs.log by default.
|
||||
if bridge.LogConnectFailures == nil then bridge.LogConnectFailures = true end
|
||||
if bridge.LogConnectTimeouts == nil then bridge.LogConnectTimeouts = false end
|
||||
bridge.ConnectFailureLogInterval = bridge.ConnectFailureLogInterval or 60
|
||||
bridge.LastConnectFailureLog = nil
|
||||
|
||||
return bridge
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_Connect()
|
||||
local now = bridge_tuning_mission_time()
|
||||
if now - self.LastConnectAttempt < (self.ConnectRetryDelay or 10) then return end
|
||||
self.LastConnectAttempt = now
|
||||
|
||||
local lib = require("socket")
|
||||
local conn = lib.tcp()
|
||||
conn:settimeout(self.ConnectTimeout or 0.02)
|
||||
|
||||
local ok, err = conn:connect(self.Host, self.Port)
|
||||
if not ok then
|
||||
if bridge_should_log_connect_failure(self, err, now) then
|
||||
self:_Log("Connect failed: " .. bridge_tuning_safe_tostring(err))
|
||||
end
|
||||
conn:close()
|
||||
return
|
||||
end
|
||||
|
||||
-- All regular bridge IO is polled from the scheduler tick and must not block DCS.
|
||||
conn:settimeout(0)
|
||||
self.Socket = conn
|
||||
self.Connected = true
|
||||
self:_Log("Connected to Python bridge")
|
||||
end
|
||||
Reference in New Issue
Block a user