DML/modules/milWings.lua
Christian Franz 07a32bd051 Version 2.2.3
Many changes, most relating to "Expansion", some community requests. Little Documentation of those changes available yet, though.
2024-05-09 09:39:22 +02:00

573 lines
18 KiB
Lua

milWings = {}
milWings.version = "0.9.5"
milWings.requiredLibs = {
"dcsCommon",
"cfxZones",
"cfxMX",
"wingTaskHelper",
}
milWings.zones = {} -- mil wings zones for flights. can have master owner
milWings.targetKeywords = { -- same as mil helo plus x
"wingTarget", -- my own zone
"milTarget", -- milH zones
"camp", -- camps
"airfield", -- airfields
"FARP", -- FARPzones
}
milWings.targets = {} -- targets for mil wings. can have master owner
-- includes wingTarget and other keywors
milWings.pureTargets = {} -- targets with wingTarget keyword
milWings.seadTargets = {}
milWings.flights = {} -- all currently active mil helo flights
milWings.ups = 1
milWings.missionTypes = {
"cas", -- standard cas
"cap", -- orbit over zone for duration
"sead", -- engage in zone for target zone's radius
"bomb",
}
function milWings.addMilWingsZone(theZone)
milWings.zones[theZone.name] = theZone
end
function milWings.addMilWingsTargetZone(theZone)
milWings.targets[theZone.name] = theZone -- overwrite if duplicate
if theZone:hasProperty("SEAD") then
milWings.seadTargets[theZone.name] = theZone
end
if theZone:hasProperty("wingTarget") then
milWings.pureTargets[theZone.name] = theZone
theZone.wingTargetName = theZone:getStringFromZoneProperty("wingTarget", "<*" .. theZone.name .. ">")
end
end
--
-- Reading / Processing milWings Zones
--
function milWings.partOfGroupDataInZone(theZone, theUnits) -- move to mx?
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 milWings.allGroupsInZoneByData(theZone) -- move to MX?
local theGroupsInZone = {}
local count = 0
for groupName, groupData in pairs(cfxMX.groupDataByName) do
if groupData.units then
if milWings.partOfGroupDataInZone(theZone, groupData.units) then
theGroupsInZone[groupName] = groupData -- DATA! work on clones!
count = count + 1
if theZone.verbose then
trigger.action.outText("+++milH: added group <" .. groupName .. "> for zone <" .. theZone.name .. ">", 30)
end
end
end
end
return theGroupsInZone, count
end
function milWings.readMilWingsZone(theZone) -- process attributes
theZone.msnType = string.lower(theZone:getStringFromZoneProperty("milWings", "cas"))
if dcsCommon.arrayContainsString(milWings.missionTypes, theZone.msnType) then
-- great, mission type is known
else
trigger.action.outText("+++milW: zone <" .. theZone.name .. ">: unknown wing mission type <" .. theZone.msnType .. ">, defaulting to 'CAS'", 30)
theZone.msnType = "cas"
end
-- see if our ownership is tied to a master
-- this adds dynamic coalition capability
if theZone:hasProperty("masterOwner") then
local mo = theZone:getStringFromZoneProperty("masterOwner")
local mz = cfxZones.getZoneByName(mo)
if not mz then
trigger.action.outText("+++milW: WARNING: Master Owner <" .. mo .. "> for zone <" .. theZone.name .. "> does not exist!", 30)
else
theZone.masterOwner = mz
end
theZone.isDynamic = theZone:getBoolFromZoneProperty("dynamic", true)
if theZone.verbose then
trigger.action.outText("milwing target <" .. theZone.name .. "> has masterOwner <" .. theZone.mz.name .. ">", 30)
if theZone.isDynamic then
trigger.action.outText("and coa is dynamically linked to owner.", 30)
end
end
end
-- get all groups inside me
local myGroups, count = milWings.allGroupsInZoneByData(theZone)
theZone.fGroups = {}
theZone.fCount = 0
-- sort into ground, helo and fixed
for groupName, data in pairs(myGroups) do
local catRaw = cfxMX.groupTypeByName[groupName]
if theZone.verbose then
trigger.action.outText("Proccing zone <" .. theZone.name .. ">: group <" .. groupName .. "> - type <" .. catRaw .. ">", 30)
end
if catRaw == "plane" then
theZone.fGroups[groupName] = data
theZone.fCount = theZone.fCount + 1
else
trigger.action.outText("+++milH: ignored group <" .. groupName .. ">: wrong type <" .. catRaw .. ">", 30)
end
end
theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 0)
theZone.hot = theZone:getBoolFromZoneProperty("hot", false)
theZone.inAir = theZone:getBoolFromZoneProperty("inAir", false)
theZone.speed = theZone:getNumberFromZoneProperty("speed", 220) -- 800 kmh
theZone.alt = theZone:getNumberFromZoneProperty("alt", 6000) -- we are always radar alt
theZone.loiter = theZone:getNumberFromZoneProperty("loiter", 3600) -- 1 hour loiter default
-- wipe all existing
for groupName, data in pairs(theZone.fGroups) do
local g = Group.getByName(groupName)
if g then
Group.destroy(g)
end
end
if theZone.verbose or milWings.verbose then
trigger.action.outText("+++milW: processed milWings zone <" .. theZone.name .. ">", 30)
end
end
function milWings.readMilWingsTargetZone(theZone)
-- can also be "camp", "farp", "airfield"
theZone.wingRadius = theZone:getNumberFromZoneProperty("wingRadius", theZone.radius)
if (not theZone.isCircle) and (not theZone:hasProperty("wingRadius")) then
-- often when we have a camp there is no cas radius, use 60km
-- and zone is ploygonal
if theZone.verbose then
trigger.action.outText("+++milH: Warning - milH target zone <" .. theZone.name .. "> is polygonal and has no CAS radius attribute. Defaulting to 80km", 30)
end
-- theZone.casRadius = 80000
theZone.wingRadius = 80000
end
if theZone:hasProperty("wingTypes") then -- if present, else all good
theZone.wingTypes = theZone:getListFromZoneProperty("wingTypes", "empty")
end
if theZone.verbose or milWings.verbose then
trigger.action.outText("+++milH: processed milWings TARGET zone <" .. theZone.name .. ">", 30)
end
if theZone:hasProperty("masterOwner") then
local mo = theZone:getStringFromZoneProperty("masterOwner")
local mz = cfxZones.getZoneByName(mo)
if not mz then
trigger.action.outText("+++milW: WARNING: Master Owner <" .. mo .. "> for zone <" .. theZone.name .. "> does not exist!", 30)
else
theZone.masterOwner = mz
end
end
end
--
-- creating flights
--
function milWings.createTakeOffWayPoint(theZone, targetZone)
local wp = {}
wp.alt = theZone.alt
wp.action = "Turning Point" -- default. overrides hot
wp.type = "Turning Point"
if not theZone.inAir then
if theZone.hot then
wp.action = "From Parking Area Hot"
wp.type = "TakeOffParkingHot"
else
wp.action = "From Parking Area"
wp.type = "TakeOffParking"
end
wp.alt = 0
wp.speed = 0
local af = dcsCommon.getClosestAirbaseTo(theZone:getPoint(), 0)
-- trigger.action.outText("closest airfield for this flight is <" .. af:getName() .. ">", 30)
wp.airdromeId = af:getID()
end
-- trigger.action.outText("flight has action <" .. wp.action .. "> and type <" .. wp.type .. ">", 30)
wp.speed = theZone.speed
local p = theZone:getPoint()
wp.x = p.x
wp.y = p.z
wp.formation_template= ""
if theZone.msnType == "cas" then
wp.task = dcsCommon.clone(wingTaskHelper.casTOTask)
-- now simply change some bits from the template
p = targetZone:getPoint()
wp.task.params.tasks[8].params.x = p.x
wp.task.params.tasks[8].params.y = p.z
if targetZone.wingRadius then -- should ALWAYS be true
wp.task.params.tasks[8].params.zoneRadius = targetZone.wingRadius
else
wp.task.params.tasks[8].params.zoneRadius = 80000
trigger.action.outText("WARNING: creating CAS flight <" .. theZone.name .. "> with no radius for target zone <" .. targetZone.name .. ">", 30)
end
elseif theZone.msnType == "cap" then
wp.task = dcsCommon.clone(wingTaskHelper.capTOTask)
p = targetZone:getPoint()
wp.task.params.tasks[6].params.x = p.x
wp.task.params.tasks[6].params.y = p.z
-- wp.task.params.tasks[6].params.zoneRadius = theZone.capRadius
if targetZone.wingRadius then -- should ALWAYS be true
wp.task.params.tasks[6].params.zoneRadius = targetZone.wingRadius
else
wp.task.params.tasks[6].params.zoneRadius = 80000
-- trigger.action.outText("WARNING: creating CAP flight <" .. theZone.name .. "> with no radius for target zone <" .. targetZone.name .. ">", 30)
end
elseif theZone.msnType == "sead" then
wp.task = dcsCommon.clone(wingTaskHelper.seadTOTask)
elseif theZone.msnType == "bomb" then
wp.task = dcsCommon.clone(wingTaskHelper.bombTOTask)
else
trigger.action.outText("milW: unknown msnType <" .. theZone.msnType .. "> in zone <" .. theZone.name .. ">", 30)
end
return wp
end
function milWings.createActionWaypoint(theZone, targetZone)
local p = targetZone:getPoint()
local wp = dcsCommon.createSimpleRoutePointData(p, theZone.alt, theZone.speed)
if theZone.msnType == "bomb" then
local task = dcsCommon.clone(wingTaskHelper.bombActionTask)
task.params.tasks[1].params.x = p.x
task.params.tasks[1].params.y = p.z
task.params.tasks[1].params.altitude = theZone.alt
task.params.tasks[1].params.speed = theZone.speed
wp.task = task
end
return wp
end
function milWings.createCommandTask(theCommand, num)
if not num then num = 1 end
local t = {}
t.enabled = true
t.auto = false
t.id = "WrappedAction"
t.number = num
local params = {}
t.params = params
local action = {}
params.action = action
action.id = "Script"
local p2 = {}
action.params = p2
p2.command = theCommand
return t
end
function milWings.createCallbackWP(gName, number, pt, alt, speed, action) -- name is group name
if not action then action = "none" end
local omwWP = dcsCommon.createSimpleRoutePointData(pt, alt, speed)
omwWP.alt_type = "BARO"
-- create a command waypoint
local task = {}
task.id = "ComboTask"
task.params = {}
local ttsk = {}
local command = "milWings.inFlightCB('" .. gName .. "', '" .. number .. "', '" .. action .."')"
ttsk[1] = milWings.createCommandTask(command,1)
task.params.tasks = ttsk
omwWP.task = task
return omwWP
end
function milWings.spawnForZone(theZone, targetZone)
-- pick one of the flight groups
if not theZone.fCount or theZone.fCount < 1 then
trigger.action.outText("+++milW: WARNING - no f-groups in zone <" .. theZone.name .. "> at spawnForZone", 30)
return nil, nil
end
local n = dcsCommon.randomBetween(1, theZone.fCount)
local theRawData = dcsCommon.getNthItem(theZone.fGroups, n)
local gData = dcsCommon.clone(theRawData)
if not gData then
trigger.action.outText("+++milW: WARNING: NIL gData in spawnForZone for <" .. theZone.name .. ">", 30)
return nil, nil
end
gData.lateActivation = false
local oName = gData.name
-- pre-process gData: names, id etc
gData.name = dcsCommon.uuid(gData.name)
local gName = gData.name
for idx, uData in pairs(gData.units) do
uData.name = dcsCommon.uuid(uData.name)
uData.alt = theZone.alt
uData.alt_type = "BARO"
uData.speed = theZone.speed
uData.unitId = nil
end
gData.groupId = nil
-- set task for group
gData.task = "CAS" -- default
if theZone.msnType == "cap" then
gData.task = "CAP"
elseif theZone.msnType == "sead" then
gData.task = "SEAD"
elseif theZone.msnType == "bomb" then
gData.task = "Ground Attack"
end
-- trigger.action.outText("main task is " .. gData.task, 30)
-- create route
local route = {}
route.routeRelativeTOT = true
route.points = {}
gData.route = route
-- create take-off waypoint for this flight
local wpTOff = milWings.createTakeOffWayPoint(theZone, targetZone)
dcsCommon.addRoutePointForGroupData(gData, wpTOff)
-- ingress point
local dest = targetZone:getPoint()
local B = dest
local A = theZone:getPoint()
local ingress = dcsCommon.pointXpercentYdegOffAB(A, B, math.random(50,80), math.random(20,50))
local omwWP = dcsCommon.createSimpleRoutePointData(ingress, theZone.alt, theZone.speed)
dcsCommon.addRoutePointForGroupData(gData, omwWP)
-- action waypoint
local awp = milWings.createActionWaypoint(theZone, targetZone)
dcsCommon.addRoutePointForGroupData(gData, awp)
-- egress
local egress = dcsCommon.pointXpercentYdegOffAB(B, A, math.random(20, 50), math.random(20,50))
local egWP = dcsCommon.createSimpleRoutePointData(egress, theZone.alt, theZone.speed)
dcsCommon.addRoutePointForGroupData(gData, egWP)
-- maybe add another to safety and then dealloc?
local final = milWings.createCallbackWP(gData.name, 4, theZone:getPoint(), theZone.alt, theZone.speed, "delete")
dcsCommon.addRoutePointForGroupData(gData, final)
-- spawn and return
local cty = dcsCommon.getACountryForCoalition(theZone.coa)
-- spawn
local groupCat = Group.Category.AIRPLANE
local theSpawnedGroup = coalition.addGroup(cty, groupCat, gData)
local theFlight = {}
theFlight.oName = oName
theFlight.spawn = theSpawnedGroup
theFlight.origin = theZone
theFlight.destination = targetZone
milWings.flights[gName] = theFlight --theSpawnedGroup
return theSpawnedGroup, gData
end
--
-- Update
--
function milWings.update()
timer.scheduleFunction(milWings.update, {}, timer.getTime() + 1)
-- update all master owners
for idx, theZone in pairs (milWings.zones) do
local mo = theZone.masterOwner
if mo then
theZone.owner = mo.owner
if theZone.isDynamic then
theZone.coa = mo.owner
end
if theZone.verbose then
trigger.action.outText("Copied master onwer <" .. mo.owner .. "> from <" .. mo.name .. "> to <" .. theZone.name .. ">", 30)
end
end
end
for idx, theZone in pairs (milWings.targets) do
local mo = theZone.masterOwner
if mo then
theZone.owner = mo.owner
if theZone.verbose then
trigger.action.outText("Copied master onwer <" .. mo.owner .. "> from <" .. mo.name .. "> to <" .. theZone.name .. ">", 30)
end
end
end
end
--
-- Event Handler
--
function milWings:onEvent(theEvent)
if not theEvent then return end
if not theEvent.initiator then return end
local theUnit = theEvent.initiator
if not theUnit.getGroup then return end
local theGroup = theUnit:getGroup()
if not theGroup then return end
local gName = theGroup:getName()
if not gName then return end
local theFlight = milWings.flights[gName]
if not theFlight then return end -- none of ours
local id = theEvent.id
if id == 4 then
-- flight landed -- milFlights currently do not land
-- except later transport flights -- we'll deal with those
-- later
-- trigger.action.outText("+++milW: flight <> landed (and removed)", 30)
if Group.isExist(theGroup) then
-- maybe schedule in a few seconds?
Group.destroy(theGroup)
end
end -- if landed
end
--
-- callback from the flight
--
function milWings.inFlightCB(gName)
-- trigger.action.outText("*****===***** callback in-flight for group <" .. gName .. ">", 30)
local theGroup = Group.getByName(gName)
if theGroup and Group.isExist(theGroup) then Group.destroy(theGroup) end
end
--
-- GC
--
function milWings.GCcollected(gName)
-- do some housekeeping?
if milWings.verbose then
trigger.action.outText("removed MIL flight <" .. gName .. ">", 30)
end
end
function milWings.GC()
timer.scheduleFunction(milWings.GC, {}, timer.getTime() + 5)
local filtered = {}
for gName, theFlight in pairs(milWings.flights) do
local theGroup = Group.getByName(gName)
if theGroup and Group.isExist(theGroup) then
-- all fine, keep it
filtered[gName] = theFlight
else
milWings.GCcollected(gName)
end
end
milWings.flights = filtered
end
--
-- API
--
function milWings.getMilWingSources(side, msnType) -- msnType is optional
if side == "red" then side = 1 end -- better safe...
if side == "blue" then side = 2 end
local sources = {}
for idx, theZone in pairs(milWings.zones) do
if theZone.coa == side then -- coa must be same side, use masterOwner for dynamism
if msnType then
if theZone.msnType == msnType then
table.insert(sources, theZone)
end
else
table.insert(sources, theZone)
end
end
end
return sources -- an array, NOT dict so we can pickrandom
end
function milWings.getMilWingTargets(side, msnType, ignoreNeutral, pure) -- gets mil targets that DO NOT belong to side
-- enter with side = -1 to get all
local source = milWings.targets
if pure then source = milWings.pureTargets end
if side == "red" then side = 1 end -- better safe...
if side == "blue" then side = 2 end
local tgt = {}
for idx, theZone in pairs(source) do
if theZone.owner ~= side then -- must NOT be owned by same side
if ignoreNeutral and theZone.owner == 0 then
-- neutral ignored
else
-- now see if we need to filter by zone's msnType
if msnType then
if theZone.wingTypes and dcsCommon.arrayContainsStringCaseInsensitive(theZone.wingTypes, msnType) then
table.insert(tgt, theZone)
end
else
table.insert(tgt, theZone)
end
end
end
end
return tgt
end
--
-- config
--
function milWings.readConfigZone()
local theZone = cfxZones.getZoneByName("milWingsConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("milWingsConfig")
end
milWings.verbose = theZone.verbose
end
--
-- Start
--
function milWings.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("cfx mil wings requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx mil wings", milWings.requiredLibs) then
return false
end
-- read config
milWings.readConfigZone()
-- process milWings Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("milWings")
for k, aZone in pairs(attrZones) do
milWings.readMilWingsZone(aZone) -- process attributes
milWings.addMilWingsZone(aZone) -- add to list
end
for idx, keyWord in pairs(milWings.targetKeywords) do
attrZones = cfxZones.getZonesWithAttributeNamed(keyWord)
for k, aZone in pairs(attrZones) do
milWings.readMilWingsTargetZone(aZone) -- process attributes
milWings.addMilWingsTargetZone(aZone) -- add to list
end
end
-- start update in 5 seconds
timer.scheduleFunction(milWings.update, {}, timer.getTime() + 1/milWings.ups)
timer.scheduleFunction(milWings.GC, {}, timer.getTime() + 1/milWings.ups * 5)
-- install event handler
world.addEventHandler(milWings)
-- say hi
trigger.action.outText("milWings v" .. milWings.version .. " started.", 30)
return true
end
if not milWings.start() then
trigger.action.outText("milWings failed to start.", 30)
milWings = nil
end