DML/modules/cfxMX.lua
Christian Franz 4de5915ea4 Version 2.4.6
Fast-roping for heloTroops
2025-03-27 16:27:52 +01:00

506 lines
20 KiB
Lua

cfxMX = {}
cfxMX.version = "3.0.1"
cfxMX.verbose = false
--[[--
Mission data decoder. Access to ME-built mission structures
Copyright (c) 2022, 2023 by Christian Franz and cf/x AG
Version History
1.2.6 - cfxMX.allTrainsByName
- train carve-outs for vehicles
2.0.0 - clean-up
- harmonized with cfxGroups
2.0.1 - groupHotByName
2.0.2 - partOfGroupDataInZone(), allGroupsInZoneByData() from milHelo
2.0.3 - allGroupsInZoneByData supports type filtering
2.1.0 - support for dynamically spawning player unit detection
- new isDynamicPlayer()
- new isMEPlayer()
- new isMEPlayerGroup()
2.2.0 - new groupCatByName[]
3.0.0 - patch coalition.addGroup() to build unit table for wasUnit
- pre-populate spawnedUnits coa, cat from MX
- spawnedUnitGroupNameByName
3.0.1 - new getClosestUnitToPoint()
--]]--
cfxMX.spawnedUnitCoaByName = {} -- reverse lookup for coas to reconstruct after kill
cfxMX.spawnedUnitCatByName = {} -- reverse lookup for cat to recon after kill
cfxMX.spawnedUnitGroupNameByName = {}
cfxMX.groupNamesByID = {}
cfxMX.groupIDbyName = {}
cfxMX.unitIDbyName = {}
cfxMX.groupCatByName = {}
cfxMX.groupDataByName = {} -- includes static groups!
cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"...
cfxMX.groupCoalitionByName = {}
cfxMX.groupHotByName = {}
cfxMX.countryByName ={} -- county of group named
cfxMX.linkByName = {}
cfxMX.allFixedByName = {}
cfxMX.allHeloByName = {}
cfxMX.allGroundByName = {}
cfxMX.allSeaByName = {}
cfxMX.allStaticByName = {}
cfxMX.allTrainsByName = {}
cfxMX.playerGroupByName = {} -- returns data only if a player is in group
cfxMX.playerUnitByName = {} -- returns data only if this is a player unit
cfxMX.playerUnit2Group = {} -- returns a group data for player units.
cfxMX.groups = {} -- all groups indexed b yname, cfxGroups folded into cfxMX
--[[-- group objects are
{
name= "",
coalition = "" (red, blue, neutral),
coanum = # (0, 1, 2 for neutral, red, blue)
category = "" (helicopter, ship, plane, vehicle, static),
hasPlayer = true/false,
playerUnits = {} (for each player unit in group: name, point, action)
}
--]]--
function cfxMX.getGroupFromDCSbyName(aName, fetchOriginal)
if not fetchOriginal then fetchOriginal = false end
for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions
local coa_name = coa_name_miz
if string.lower(coa_name_miz) == 'neutrals' then -- remove 's' at neutralS
coa_name = 'neutral'
end
-- directly convert coalition into number for easier access later
local coaNum = 0
if coa_name == "red" then coaNum = 1 end
if coa_name == "blue" then coaNum = 2 end
if type(coa_data) == 'table' then -- coalition = {bullseye, nav_points, name, county},
-- with county being an array
if coa_data.country then -- make sure there a country table for this coalition
for cntry_id, cntry_data in pairs(coa_data.country) do -- iterate all countries for this
-- per country = {id, name, vehicle, helicopter, plane, ship, static}
local countryName = string.lower(cntry_data.name)
local countryID = cntry_data.id
if type(cntry_data) == 'table' then -- filter strings .id and .name
for obj_type_name, obj_type_data in pairs(cntry_data) do
if obj_type_name == "helicopter" or
obj_type_name == "ship" or
obj_type_name == "plane" or
obj_type_name == "vehicle" or
obj_type_name == "static"
-- note: trains are 'vehicle' here
then -- (so it's not id or name)
local category = obj_type_name
if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one group!
for group_num, group_data in pairs(obj_type_data.group) do
if group_data.name == aName then
local theGroup = group_data
-- usually we return a copy of this
if not fetchOriginal then
theGroup = dcsCommon.clone(group_data)
end
-- train carve-out: if first unit's type == "Train", change
-- category to "train"
if group_data.units[1] and group_data.units[1].type == "Train" then category = "train" end
return theGroup, category, countryID
end
end
end --if has category data
end --if plane, helo etc... category
end --for all objects in country
end --if has country data
end --for all countries in coalition
end --if coalition has country table
end -- if there is coalition data
end --for all coalitions in mission
return nil, "none", "none"
end
function cfxMX.getStaticFromDCSbyName(aName, fetchOriginal)
if not fetchOriginal then fetchOriginal = false end
-- fetch the static description for static named aName (if exists)
-- returned structure must be parsed for useful information
-- returns data, category, countyID and parent group name
-- unless fetchOriginal is true, creates a deep clone of
-- static data structure
for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions
local coa_name = coa_name_miz
if string.lower(coa_name_miz) == 'neutrals' then -- remove 's' at neutralS
coa_name = 'neutral'
end
-- directly convert coalition into number for easier access later
local coaNum = 0
if coa_name == "red" then coaNum = 1 end
if coa_name == "blue" then coaNum = 2 end
if type(coa_data) == 'table' then -- coalition = {bullseye, nav_points, name, county},
-- with county being an array
if coa_data.country then -- make sure there a country table for this coalition
for cntry_id, cntry_data in pairs(coa_data.country) do -- iterate all countries for this
-- per country = {id, name, vehicle, helicopter, plane, ship, static}
local countryName = string.lower(cntry_data.name)
local countryID = cntry_data.id
if type(cntry_data) == 'table' then -- filter strings .id and .name
for obj_type_name, obj_type_data in pairs(cntry_data) do
if obj_type_name == "static"
then -- (only look at statics)
local category = obj_type_name
if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one static in group!
for group_num, group_data in pairs(obj_type_data.group) do
-- get linkUnit info if it exists
local linkUnit = nil
if group_data and group_data.route and group_data.route and group_data.route.points[1] then
linkUnit = group_data.route.points[1].linkUnit
if linkUnit then
end
end
if group_data and group_data.units and type(group_data.units) == 'table'
then --make sure - again - that this is a valid group
for unit_num, unit_data in pairs(group_data.units) do -- iterate units
if unit_data.name == aName then
local groupName = group_data.name
local theStatic = unit_data
if not fetchOriginal then
theStatic = dcsCommon.clone(unit_data)
-- copy group ID from group above
theStatic.groupId = group_data.groupId
-- copy linked unit data
theStatic.linkUnit = linkUnit
end
return theStatic, category, countryID, groupName
end -- if name match
end -- for all units
end -- has groups
end -- is a static
end --if has category data
end --if plane, helo etc... category
end --for all objects in country
end --if has country data
end --for all countries in coalition
end --if coalition has country table
end -- if there is coalition data
end --for all coalitions in mission
return nil, "<none>", "<none>", "<no group name>"
end
function cfxMX.createCrossReferences()
for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions
local coa_name = coa_name_miz
if string.lower(coa_name_miz) == 'neutrals' then -- remove 's' at neutralS
coa_name = 'neutral'
end
-- directly convert coalition into number for easier access later
local coaNum = 0
if coa_name == "red" then coaNum = 1 end
if coa_name == "blue" then coaNum = 2 end
if type(coa_data) == 'table' then -- coalition = {bullseye, nav_points, name, county},
-- with county being an array
if coa_data.country then -- make sure there a country table for this coalition
for cntry_id, cntry_data in pairs(coa_data.country) do -- iterate all countries for this
-- per country = {id, name, vehicle, helicopter, plane, ship, static}
local countryName = string.lower(cntry_data.name)
local countryID = cntry_data.id
if type(cntry_data) == 'table' then -- filter strings .id and .name
for obj_type_name, obj_type_data in pairs(cntry_data) do
local gCat = -1 -- "illegal"
if obj_type_name == "helicopter" then gCat = 1
elseif obj_type_name == "ship" then gCat = 3
elseif obj_type_name == "plane" then gCat = 0
elseif obj_type_name == "vehicle" then gCat = 2
else -- if obj_type_name == "static" -- what about "cargo"?
gCat = -1 -- just for safety. no cat for static, train, cargo
end
if obj_type_name == "helicopter" or
obj_type_name == "ship" or
obj_type_name == "plane" or
obj_type_name == "vehicle" or
obj_type_name == "static" -- what about "cargo"?
-- note that trains appear as 'vehicle'
then -- (so it's not id or name)
local category = obj_type_name
if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one group!
for group_num, group_data in pairs(obj_type_data.group) do
local aName = group_data.name
local aID = group_data.groupId
-- get linkUnit info if it exists
local linkUnit = nil
local isHot = false
if group_data and group_data.route and group_data.route and group_data.route.points[1] then
linkUnit = group_data.route.points[1].linkUnit
cfxMX.linkByName[aName] = linkUnit
local action = group_data.route.points[1].action
if action then
isHot = dcsCommon.stringEndsWith(action, "Hot")
end
end
cfxMX.groupHotByName[aName] = isHot
if group_data.units[1] and group_data.units[1].type == "Train" then
category = "train"
obj_type_name = "train"
end
cfxMX.groupTypeByName[aName] = category
cfxMX.groupNamesByID[aID] = aName
cfxMX.groupIDbyName[aName] = aID
cfxMX.groupDataByName[aName] = group_data
cfxMX.countryByName[aName] = countryID -- !!! was cntry_id
cfxMX.groupCoalitionByName[aName] = coaNum
-- now make the type-specific xrefs
if obj_type_name == "helicopter" then
cfxMX.allHeloByName[aName] = group_data
cfxMX.groupCatByName[aName] = 1
elseif obj_type_name == "ship" then
cfxMX.allSeaByName[aName] = group_data
cfxMX.groupCatByName[aName] = 3
elseif obj_type_name == "plane" then
cfxMX.allFixedByName[aName] = group_data
cfxMX.groupCatByName[aName] = 0
elseif obj_type_name == "vehicle" then
cfxMX.allGroundByName[aName] = group_data
cfxMX.groupCatByName[aName] = 2
elseif obj_type_name == "static" then
cfxMX.allStaticByName[aName] = group_data
-- cfxMX.groupCatByName[aName] = -1 -- not covered
elseif obj_type_name == "train" then
cfxMX.allTrainsByName[aName] = group_data
cfxMX.groupCatByName[aName] = 4
else
-- should be impossible, but still
trigger.action.outText("+++MX: <" .. obj_type_name .. "> unknown type for <" .. aName .. ">", 30)
end
-- now iterate all units in this group
-- for unit xref like player info and ID
local hasPlayer = false
local playerUnits = {}
local groupName = group_data.name
if env.mission.version > 7 then -- translate raw to actual
groupName = env.getValueDictByKey(groupName)
end
for unit_num, unit_data in pairs(group_data.units) do
if unit_data.skill then
if unit_data.skill == "Client" or unit_data.skill == "Player" then
-- player unit
cfxMX.playerUnitByName[unit_data.name] = unit_data
cfxMX.playerGroupByName[aName] = group_data -- inefficient, but works
cfxMX.playerUnit2Group[unit_data.name] = group_data
hasPlayer = true
local playerData = {}
playerData.name = unit_data.name
playerData.point = {}
playerData.point.x = unit_data.x
playerData.point.y = 0
playerData.point.z = unit_data.y
playerData.action = "none" -- default
-- access initial waypoint data by 'reaching up'
-- into group data and extract route.points[1]
if group_data.route and group_data.route.points and (#group_data.route.points > 0) then
playerData.action = group_data.route.points[1].action
end
table.insert(playerUnits, playerData)
end -- if unit skill client
end -- if has skill
cfxMX.unitIDbyName[unit_data.name] = unit_data.unitId
if gCat >= 0 then -- pre-populate table
cfxMX.spawnedUnitCoaByName[unit_data.name] = coaNum
cfxMX.spawnedUnitCatByName[unit_data.name] = gCat
end
cfxMX.spawnedUnitGroupNameByName[unit_data.name] = groupName
end -- for all units
local entry = {}
entry.name = groupName
entry.coalition = coa_name
entry.coaNum = coaNum
entry.category = category
entry.hasPlayer = hasPlayer
entry.playerUnits = playerUnits
-- add to db
cfxMX.groups[groupName] = entry
end -- for all groups
end --if has category data
end --if plane, helo etc... category
end --for all objects in country
end --if has country data
end --for all countries in coalition
end --if coalition has country table
end -- if there is coalition data
end --for all coalitions in mission
end
-- return all groups that can have players in them
-- includes groups that currently are not or not anymore alive
function cfxMX.getPlayerGroup()
local playerGroups = {}
for gName, gData in pairs (cfxMX.groups) do
if gData.hasPlayer then
table.insert(playerGroups, gData)
end
end
return playerGroups
end
-- return all group names that can have players in them
-- includes groups that currently are not or not anymore alive
function cfxMX.getPlayerGroupNames()
local playerGroups = {}
for gName, gData in pairs (cfxMX.groups) do
if gData.hasPlayer then
table.insert(playerGroups, gName)
end
end
return playerGroups
end
function cfxMX.catText2ID(inText)
local outCat = 0 -- airplane
local c = inText:lower()
if c == "helicopter" then outCat = 1 end
if c == "ship" then outCat = 3 end
if c == "plane" then outCat = 0 end -- redundant
if c == "vehicle" then outCat = 2 end
if c == "train" then outCat = 4 end
if c == "static" then outCat = -1 end
return outCat
end
function cfxMX.partOfGroupDataInZone(theZone, theUnits) -- move to mx?
--local zP --= cfxZones.getPoint(theZone)
local zP = theZone:getDCSOrigin() -- don't use getPoint now.
zP.y = 0
for idx, aUnit in pairs(theUnits) do
local uP = {}
uP.x = aUnit.x
uP.y = 0
uP.z = aUnit.y -- !! y-z
if theZone:pointInZone(uP) then return true end
end
return false
end
function cfxMX.allGroupsInZoneByData(theZone, cat) -- returns groups indexed by name and count
if not cat then cat = {"helicopter", "ship", "plane", "vehicle" } end
if type(cat) == "string" then cat = {cat} end
local theGroupsInZone = {}
local count = 0
for groupName, groupData in pairs(cfxMX.groupDataByName) do
local gType = cfxMX.groupTypeByName[groupName]
if dcsCommon.arrayContainsString(cat, gType) and groupData.units then
if cfxMX.partOfGroupDataInZone(theZone, groupData.units) then
theGroupsInZone[groupName] = groupData -- DATA! work on clones!
count = count + 1
if theZone.verbose then
trigger.action.outText("+++cfxMX: added group <" .. groupName .. "> for zone <" .. theZone.name .. ">", 30)
end
end
end
end
return theGroupsInZone, count
end
function cfxMX.getClosestUnitToPoint(A, filter) -- uses MX!
if not A then return nil end
if not filter then filter = "all" end
local theUnit = nil
local uIdx = nil
local theGroup = nil
local ax = A.x
local az = A.z
local closest = math.huge
for name, gData in pairs(cfxMX.groupDataByName) do
if filter == "all" or filter == cfxMX.groupTypeByName[name] then
for idx, uData in pairs(gData.units) do
-- use square delta, immediate
local dx = ax - uData.x
local dz = az - uData.y -- !!
local d = dx * dx + dz * dz
if d < closest then
theUnit = uData
theGroup = gData
closest = d
uIdx = idx
end
end
end
end
return theUnit, theGroup, uIdx, closest^0.5
end
function cfxMX.isDynamicPlayer(theUnit)
if not theUnit then return false end
if not theUnit.getName then return false end
if not theUnit.getPlayerName then return false end
if not theUnit:getPlayerName() then return false end
local uName = theUnit:getName()
if cfxMX.playerUnitByName[uName] then return false end
return true
end
function cfxMX.isMEPlayer(theUnit)
if not theUnit then return false end
if not theUnit.getName then return false end
if not theUnit.getPlayerName then return false end
if not theUnit:getPlayerName() then return false end
local uName = theUnit:getName()
if cfxMX.playerUnitByName[uName] then return true end
return false
end
function cfxMX.isMEPlayerGroup(theUnit)
if not theUnit then return false end
if not theUnit.getName then return end
if not theUnit.getPlayerName then return end
local uName = theUnit:getName()
if cfxMX.playerUnitByName[uName] then return true end
return false
end
function cfxMX.start()
cfxMX.createCrossReferences()
if cfxMX.verbose then
trigger.action.outText("cfxMX: "..#cfxMX.groupNamesByID .. " groups processed successfully", 30)
end
trigger.action.outText("cfxMX v." .. cfxMX.version .. " started.", 30)
end
--
-- patch coalition.addGroup so we can record all units by name for their coalition
--
coalition.mxAddGroup = coalition.addGroup -- save old
function coalition.addGroup(cty, cat, data) -- patch addGroup to note all spawned units for DCS static switch-a-roo
local g = coalition.mxAddGroup(cty, cat, data)
if not g then return nil end
local coa = coalition.getCountryCoalition(cty)
local units = g:getUnits()
local gName = g:getName()
for idx, u in pairs(units) do
uName = u:getName()
cfxMX.spawnedUnitCoaByName[uName] = coa
cfxMX.spawnedUnitCatByName[uName] = cat
cfxMX.spawnedUnitGroupNameByName[uName] = gName
-- trigger.action.outText("MX: Unit <" .. uName .. "> spawned for coa <" .. coa .. ">", 30)
end
return g
end
-- start
cfxMX.start()