This commit is contained in:
Frank
2026-08-08 10:23:38 +02:00
parent 7026a94301
commit a46bbcc2d2
9 changed files with 5444 additions and 0 deletions
+9
View File
@@ -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