DML/modules/convoy.lua
Christian Franz 88026bf851 Version 2.3.8
Usher
2024-12-05 10:11:24 +01:00

1079 lines
34 KiB
Lua

convoy = {}
convoy.version = "1.3.0"
convoy.requiredLibs = {
"dcsCommon",
"cfxZones",
"cfxMX",
}
convoy.zones = {}
convoy.convoys = {} -- running convoys
convoy.ups = 1
convoy.convoyWPReached = {}
convoy.convoyAttacked = {}
convoy.convoyDestroyed = {}
convoy.convoyArrived = {}
convoy.roots = {} -- group comms
convoy.uuidNum = 1
--[[--
A DML module (C) 2024 by Christian Franz
VERSION HISTORY
1.0.0 - Initial version
1.1.0 - MUCH better reporting for all coalitions
- remaining unit count on successful attack in reports
- warning when arriving at penultimate
- corrected method for polling when dead etc.
- anon name for reporting
- anon uuid
- actionSound
- support for attachTo:
- warning when not onStart and no start?
- say hi
- removed destination attribute
1.2.0 - convoyMethod
- convoyTriggerMethod
1.3.0 - typo 'destination'
- start & arrival messages only with wpUpdates
- new 'listOwn' config attribute
- convoy destroyed message only if listOwn
- own convoys only listed if listOwn
--]]--
--[[-- CONVOY Structure
.name (uuid) name of convoy
.dest destination string, defaulted from theZone.destinations
.destObject destination object from theZone.destobject
.waypoints array of vec3 points, one for each wp
.currWP -- index of last WP reached, inited to 1 on start
.origin zone where convoy spawned
.groups array groups (only one element) that this convoy consists of, ground only
.helos array if defined, helicopters escorting.0 One group only
.groupSizes dict by groupname of group size to detect loss, updated
.lastAttackReport in time seconds since last report to enable pauses between reports
.coa coalition this convay belongs to
.reached indexed by waypoint. true if we reached that wp.
.distance = total length of route
.oName = original name of gorup
.desc = name, from, to as text
.wasAttacked true after first successful attack, for remain count
--]]--
--
-- Misc
--
function convoy.uuid()
convoy.uuidNum = convoy.uuidNum + 1
return convoy.uuidNum
end
--
-- callbacks
--
function convoy.installWPCallback(theCB)
table.insert(convoy.convoyWPReached, theCB)
end
function convoy.invokeWPCallbacks(theConvoy, wp, wpnum)
for idx, cb in pairs(convoy.convoyWPReached) do
cb(theConvoy, wp, wpnum)
end
end
function convoy.installAttackCallback(theCB)
table.insert(convoy.convoyAttacked, theCB)
end
function convoy.invokeAttackedCallbacks(theConvoy)
for idx, cb in pairs(convoy.convoyAttacked) do
cb(theConvoy)
end
end
function convoy.installDestroyed(theCB)
table.insert(convoy.convoyDestroyed, theCB)
end
function convoy.invokeDestroyedCallbacks(theConvoy)
for idx, cb in pairs(convoy.convoyDestroyed) do
cb(theConvoy)
end
end
function convoy.installArrived(theCB)
table.insert(convoy.convoyArrived, theCB)
end
function convoy.invokeArrivedCallbacks(theConvoy)
for idx, cb in pairs(convoy.convoyArrived) do
cb(theConvoy)
end
end
--
-- Reading Zones
--
function convoy.addConvoyZone(theZone)
convoy.zones[theZone.name] = theZone
end
function convoy.thereCanOnlyBeOne(theDict)
for key, value in pairs (theDict) do
local ret = {}
ret[key] = value
return ret
end
end
function convoy.readConvoyZone(theZone)
theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 0)
theZone.isDynamic = theZone:getBoolFromZoneProperty("dynamic", false)
-- get groups inside me.
local myGroups, count = cfxMX.allGroupsInZoneByData(theZone, {"vehicle"})
local myHelos = {} -- for spawning only.
local myHelos, hcount = cfxMX.allGroupsInZoneByData(theZone, {"helicopter"})
if count < 1 then
trigger.action.outText("cnvy: WARNING: convoy zone <" .. theZone.name .. "> has no vehicles.", 30)
end
-- process destinations for each vehicle group
local destinations = {}
local destObjects = {}
local distances = {}
--local froms = {}
for gName, gData in pairs (myGroups) do
local dest, destObj = convoy.getDestinationForData(gData)
destinations[gName] = dest
destObjects[gName] = destObj -- nearest DCS/DML objects
distances[gName] = convoy.getDistanceForData(gData)
--froms[gName] = convoy.getSourceForData(theZone)
end
theZone.myGroups = myGroups -- vehicles only. only one chosen per spawn
-- detination calc'd on-demand if nil
-- dict by name
theZone.destinations = destinations
theZone.destObjects = destObjects
theZone.distances = distances
theZone.froms = convoy.getSourceForData(theZone)
theZone.myHelos = myHelos -- helos can only escort, don't count
-- helos wonky with multi-groups because dcs
-- linking to groups
theZone.identical = theZone:getBoolFromZoneProperty("identical", false)
theZone.unique = not theZone.identical
theZone.preWipe = theZone:getBoolFromZoneProperty("preWipe", false) or theZone.identical
theZone.endWipe = theZone:getBoolFromZoneProperty("endWipe", true)
theZone.killWipeDelay = theZone:getNumberFromZoneProperty("killWipeDelay", 300) -- leave helos as angry hornets for a while (5 Min = 300s)
theZone.pEscort = theZone:getNumberFromZoneProperty("pEscort", 100) -- in percent (= * 100)
theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false)
theZone.wpUpdates = theZone:getBoolFromZoneProperty("wpUpdates", true)
theZone.attackWarnings = theZone:getBoolFromZoneProperty("attackWarnings", true)
if theZone:hasProperty("spawn?") then
theZone.spawnFlag = theZone:getStringFromZoneProperty("spawn?", "none")
theZone.lastSpawnFlag = trigger.misc.getUserFlag(theZone.spawnFlag)
else
if not theZone.onStart then
trigger.action.outText("+++CVY: Warning: Convoy zone <" .. theZone.name .. "> has disabled 'onStart' and has no 'spawn?' input. This convoy zone can't send out any convoys,", 30)
end
end
theZone.convoyMethod = theZone:getStringFromZoneProperty("convoyMethod", "inc")
theZone.convoyTriggerMethod = theZone:getStringFromZoneProperty("convoyTriggerMethod", "change")
--[[--
if theZone:hasProperty("destination") then -- remove me
theZone.destination = theZone:getStringFromZoneProperty("destination", "none")
end
--]]--
if theZone:hasProperty("dead!") then
theZone.deadOut = theZone:getStringFromZoneProperty("dead!", "none")
end
if theZone:hasProperty("attacked!") then
theZone.attackedOut = theZone:getStringFromZoneProperty("attacked!", "none")
end
if theZone:hasProperty("arrived!") then
theZone.arrivedOut = theZone:getStringFromZoneProperty("arrived!", "none")
end
-- wipe all existing vehicle and helos
for groupName, data in pairs(myGroups) do
local g = Group.getByName(groupName)
if g then
Group.destroy(g)
end
end
for groupName, data in pairs(myHelos) do
local g = Group.getByName(groupName)
if g then
Group.destroy(g)
end
end
end
function convoy.getDistanceForData(theData)
local total = 0
if not theData then return 0 end
local route = theData.route
local points = route.points
local wpNum = #points
if wpNum < 2 then return 0 end
local i = 1
local t = points[1]
local last = {x=t.x, y = 0, z = t.y}
while i < wpNum do
i = i + 1
local t = points[i]
local now = {x=t.x, y = 0, z = t.y}
total = total + dcsCommon.dist(last, now)
last = now
end
return total
end
function convoy.getLocName(p) -- returns a string and bool (success)
local msg = ""
local success = false
if twn and towns then
local name, data, dist = twn.closestTownTo(p)
local mdist= dist * 0.539957
dist = math.floor(dist/100) / 10
mdist = math.floor(mdist/100) / 10
local bear = dcsCommon.compassPositionOfARelativeToB(p, data.p)
msg = dist .. "km/" .. mdist .."nm " .. bear .. " of " .. name
success = true
end
return msg, success
end
function convoy.getSourceForData(theZone)
if twn and towns then
local currPoint = theZone:getPoint()
local name, data, dist = twn.closestTownTo(currPoint)
local mdist= dist * 0.539957
dist = math.floor(dist/100) / 10
mdist = math.floor(mdist/100) / 10
local bear = dcsCommon.compassPositionOfARelativeToB(currPoint, data.p)
return dist .. "km/" .. mdist .. "nm " .. bear .. " of " .. name
end
return theZone.name
end
function convoy.getDestinationForData(theData)
-- dest is filled with town data if available
-- destObject is nearest dmlZone or airfield, whatever closest
local dest = "unknown"
local destObj
local destType
if theData then
-- access route points
local route = theData.route
local points = route.points
local wpnum = #points
local lastWP = points[wpnum]
local thePoint = {x=lastWP.x, y=0, z=lastWP.y} -- !!!
local hasTwn = false
dest, hasTwn = convoy.getLocName(thePoint)
local clsZ, zDelta = cfxZones.getClosestZone(thePoint)
local clsA, aDelta = dcsCommon.getClosestAirbaseTo(thePoint, 0)
local dist = zDelta
destType = "dmlZone"
destObj = clsZ
if aDelta < dist then -- airfield is closer than closest zone
dist = aDelta
destType = "airfield"
destObj = clsA
end
if convoy.verbose then
trigger.action.outText(theData.name .. " has destination object " .. destObj:getName(), 30)
end
if not hasTwn then
-- use nearest dmlZone or airfield to name destination
dest = destObj:getName()
if dist < 10000 then
elseif dist < 20000 then
dest = dest .. " area"
else
dest = "greater " .. dest .. " area"
end
end
else
dest = "NO GROUP DATA"
end
return dest, destObj, destType
end
function convoy.startConvoy(theZone, groupIdent)
-- groupIdent overrides random selection and pickes exactly that group
-- make sure my coa is set up correctly
-- groupIdent is a string (group name)
if theZone.isDynamic then
theZone.coa = theZone:getCoalition() -- auto-resolves masterowner
end
-- pre-wipe existing convoys if they exist, will NOT cause
-- failed event!
if theZone.preWipe then
local groupCollector = {}
local filtered = {}
for cName, entry in pairs(convoy.convoys) do
if entry.origin == theZone then
for gName, theGroup in pairs(entry.groups) do -- vehicles
if Group.isExist(theGroup) then
table.insert(groupCollector, theGroup)
end
end
for gName, theGroup in pairs(entry.helos) do -- helicopters
if Group.isExist(theGroup) then
table.insert(groupCollector, theGroup)
end
end
-- do not pass on
else
filtered[cName] = entry -- pass on
end
end
-- delete the groups that are still alive
for idx, theGroup in pairs(groupCollector) do
trigger.action.outText("cnvy: prewipe - removing group <" .. theGroup:getName() .. ">", 30)
Group.destroy(theGroup)
end
convoy.convoys = filtered
end
-- iterate all groups and spawn them
local spawns = {} -- vehicle groups -- DICT
local helos = {}
local spawnSizes = {} -- overkill, just one group in here
local theConvoy = {} -- data carrier
theConvoy.name = dcsCommon.uuid(theZone.name)
theConvoy.anon = "CVY-" .. convoy.uuid()
-- choose a vehicle group from all available
local gOrig
if groupIdent then gOrig = theZone.myGroups[groupIdent]
else
local allGroups = dcsCommon.enumerateTable(theZone.myGroups)
gOrig = dcsCommon.pickRandom(allGroups)
end
local gName = gOrig.name
local gData = dcsCommon.clone(gOrig)
-- make unique names for group and units if desired
if theZone.unique then
gData.name = dcsCommon.uuid(gOrig.name)
gData.groupId = nil
for idx, theUnit in pairs (gData.units) do
theUnit.name = dcsCommon.uuid(theUnit.name)
theUnit.unitId = nil
end
end
-- retrieve destination from zone
local dest = theZone.destination
if not dest then
dest = theZone.destinations[gName]
end
local from = theZone.froms -- they are all the same...
theConvoy.dest = dest
theConvoy.destObject = theZone.destObjects[gName]
theConvoy.desc = theConvoy.name .. " from " .. from .. " to " .. dest
-- spawn one vehicle group and add it to my spawns and spawnSizes
gCat = Group.Category.GROUND
local waypoints = convoy.amendVehicleData(theZone, gData, theConvoy.name) -- add actions to route, return waypoint locations
theConvoy.waypoints = waypoints
local cty = dcsCommon.getACountryForCoalition(theZone.coa)
local theSpawnedGroup = coalition.addGroup(cty, gCat, gData)
spawns[gData.name] = theSpawnedGroup
spawnSizes[gData.name] = theSpawnedGroup:getSize()
theConvoy.distance = theZone.distances[gOrig.name]
theConvoy.oName = gOrig.name
-- now spawn one helo group and make them escort the group that was
-- just spawned
local rnd = math.random(1,100)
if (rnd <= theZone.pEscort) and dcsCommon.getSizeOfTable(theZone.myHelos) > 0 then
gCat = Group.Category.HELICOPTER -- allow escort helos
allGroups = dcsCommon.enumerateTable(theZone.myHelos)
gOrig = dcsCommon.pickRandom(allGroups)
gData = dcsCommon.clone(gOrig)
-- make group unique
if theZone.unique then
gData.name = dcsCommon.uuid(gOrig.name)
gData.groupId = nil
for idx, theUnit in pairs (gData.units) do
theUnit.name = dcsCommon.uuid(theUnit.name)
theUnit.unitId = nil
end
end
convoy.makeHeloDataEscortGroup(gData, theSpawnedGroup)
local theSpawnedHelos = coalition.addGroup(cty, gCat, gData)
helos[gData.name] = theSpawnedHelos
end
theConvoy.origin = theZone
theConvoy.groups = spawns -- contains only one group
theConvoy.helos = helos -- all helos spawned
theConvoy.groupSizes = spawnSizes -- vehicle group size by name
theConvoy.lastAttackReport = -1000
theConvoy.coa = theZone.coa
theConvoy.reached = {} -- waypoints reached message remember
-- add to convoys
convoy.convoys[theConvoy.name] = theConvoy
-- return the convoy entry
return theConvoy
end
function convoy.amendVehicleData(theZone, theData, convoyName)
-- place a callback action for each waypoint
-- in data block
if not theData.route then return nil end
local route = theData.route
if not route.points then return nil end
local points = route.points
local np = #points
if np < 1 then return nil end
local newPoints = {}
local waypoints = {}
for idx=1, np do
local wp = points[idx]
local tasks = wp.task.params.tasks
local tnew = #tasks + 1 -- new number for this task
local t = {
["number"] = tnew,
["auto"] = false,
["id"] = "WrappedAction",
["enabled"] = true,
["params"] = {
["action"] = {
["id"] = "Script",
["params"] = {
["command"] = "convoy.wpReached(\"" .. theData.name .."\", \"" .. convoyName .. "\", \"" .. idx .. "\", \"" .. np .. "\")",
}, -- end of ["params"]
}, -- end of ["action"]
}, -- end of ["params"]
} -- end of task
-- add t to tasks
table.insert(tasks, t)
newPoints[idx] = wp
local thePoint = {x=wp.x, y=0, z=wp.y}
waypoints[idx] = thePoint
end
route.points = newPoints
return waypoints
end
function convoy.makeHeloDataEscortGroup(theData, theGroup)
-- overwrite entire route with new escort mission for theGroup
local gID = theGroup:getID()
-- set group's main task to CAS
theData.tasks = {}
theData.task = "CAS"
local nuPoints = {}
local oldPoints = theData.route.points
local wp1 = dcsCommon.clone(oldPoints[1]) -- clone old
wp1.alt = 100 -- overwrite key data
wp1.action = "Turning Point"
wp1.alt_type = "RADIO"
wp1.speed = 28
wp1.task = {
["id"] = "ComboTask",
["params"] = {
["tasks"] = {
[1] = {
["enabled"] = true,
["key"] = "CAS",
["id"] = "EngageTargets",
["number"] = 1,
["auto"] = true,
["params"] = {
["targetTypes"] =
{
[1] = "Helicopters",
[2] = "Ground Units",
[3] = "Light armed ships",
}, -- end of ["targetTypes"]
["priority"] = 0,
}, -- end of ["params"]
}, -- end of [1]
[2] = {
["enabled"] = true,
["auto"] = false,
["id"] = "GroundEscort",
["number"] = 2,
["params"] = {
["targetTypes"] = {
[1] = "Helicopters",
[2] = "Ground Units",
}, -- end of ["targetTypes"]
["groupId"] = gID, -- ESCORT THIS!
["lastWptIndex"] = 2,
["engagementDistMax"] = 500,
["lastWptIndexFlag"] = false,
["lastWptIndexFlagChangedManually"] = false,
}, -- end of ["params"]
}, -- end of [2]
}, -- end of ["tasks"]
}, -- end of ["params"]
} -- end of ["task"]
wp1.type = "Turning Point"
nuPoints[1] = wp1
theData.route.points = nuPoints
end
--
-- WP Callback
--
function convoy.wpReached(gName, convName, idx, wpNum)
idx = tonumber(idx)
wpNum = tonumber(wpNum)
local theConvoy = convoy.convoys[convName]
if not theConvoy then
trigger.action.outText("convoy <" .. convName .. "> not found, exiting", 30)
return
end
local waypoints = theConvoy.waypoints
theConvoy.currWP = idx
local coa = theConvoy.coa
local enemy = 1
if coa == 1 then enemy = 2 end
local theZone = theConvoy.origin
if theConvoy.reached[idx] then
trigger.action.outText("<" .. convName .. ">: We've been here before...?", 30)
else
convoy.invokeWPCallbacks(theConvoy, idx, wpNum)
theConvoy.reached[idx] = true -- remember we were reported this
if idx == 1 then
local distk = math.floor(theConvoy.distance / 1000 + 1.5)
local distm = math.floor(0.621371 * theConvoy.distance/1000 + 1)
if theZone.wpUpdates or convoy.listOwn then
trigger.action.outTextForCoalition(coa, "Convoy " .. convName .. " has departed from rallying point " .. theZone.froms .. " towards their destination " .. theConvoy.dest .. " (for a total distance of " .. distk .. "km/" .. distm .. "nm).", 30)
trigger.action.outSoundForCoalition(coa, convoy.actionSound)
end
if convoy.listEnemy then
local msg = "Intelligence reports new enemy convoy " .. theConvoy.anon .. " enroute to " .. theConvoy.destObject:getName()
trigger.action.outTextForCoalition(enemy, msg, 30)
trigger.action.outSoundForCoalition(enemy, convoy.actionSound)
end
elseif idx == wpNum then
if theZone.wpUpdates or convoy.listOwn then
trigger.action.outTextForCoalition(coa, "Convoy " .. convName .. " has arrived at destination (" .. theConvoy.dest .. ").", 30)
trigger.action.outSoundForCoalition(coa, convoy.actionSound)
end
if convoy.listEnemy then
local msg = "Enemy convoy " .. theConvoy.anon .. " arrived at " .. theConvoy.destObject:getName()
trigger.action.outTextForCoalition(enemy, msg, 30)
trigger.action.outSoundForCoalition(enemy, convoy.actionSound)
end
convoy.invokeArrivedCallbacks(theConvoy)
-- hit the output flag if defined
if theZone.arrivedOut then
theZone:pollFlag(theZone.arrivedOut, theZone.convoyMethod) -- "inc")
end
-- remove convoy from watchlist
convoy.convoys[convName] = nil
-- deallocate convoy if theZone requests is
if theZone.endWipe then
convoy.wipeConvoy(theConvoy)
end
else
if theZone.wpUpdates then
local p = waypoints[idx] -- idx is one-based!
local msg = "Convoy " .. convName .. ", enroute to destination " .. theConvoy.dest .. ", has reached "
local locName, hasLoc = convoy.getLocName(p)
if hasLoc then
msg = msg .. "checkpoint located at " .. locName .. " (waypoint " .. idx .. " of " .. wpNum .. ")."
else
msg = msg .. "waypoint " ..idx .. " of " .. wpNum .. "."
end
trigger.action.outTextForCoalition(coa, msg, 30)
trigger.action.outSoundForCoalition(theConvoy.coa, convoy.actionSound)
end
end
end
end
function convoy.wipeConvoy(theConvoy) -- called async and sync
local theZone = theConvoy.origin
if convoy.verbose or theZone.verbose then
trigger.action.outText("+++cnvy: entere wipe for convoy <" .. theConvoy.name .. "> started from <" .. theZone.name .. ">", 30)
end
for gName, theGroup in pairs(theConvoy.groups) do
if Group.isExist(theGroup) then
Group.destroy(theGroup)
end
end
for gName, theGroup in pairs(theConvoy.helos) do
if Group.isExist(theGroup) then
Group.destroy(theGroup)
end
end
end
--
-- API
--
function convoy.collectConvoysFor(coa)
local collector = {}
for idx, theZone in pairs(convoy.zones) do
if theZone.isDynamic then
theZone.coa = theZone:getCoalition()
end
-- warning: differentiating between coa and owner!
if theZone.coa == coa then
table.insert(collector, theZone)
end
end
return collector
end
function convoy.sourceAndDestinationForCoa(theList, coa, allowNeutral)
local solutions = {}
for idx, theZone in pairs(theList) do
-- all destinations have a coalition
for gName, theObject in pairs(theZone.destObjects) do
-- destObjects can be dml zones or airbases
local oCoa = theObject:getCoalition() -- dmlZones return Owner and respect masterowner
if oCoa == coa or (allowNeutral and oCoa == 0) then
local aMatch = {theZone=theZone, gName=gName}
table.insert(solutions, aMatch)
end
end
end
return solutions
end
function convoy.filterConvoysByDistance(theList, maxDist)
local filtered = {}
for idx, theEntry in pairs(theList) do
local theZone = theEntry.theZone
local gName = theEntry.gName
local cDist = theZone.distances[gName]
if cDist < maxDist then
table.insert(filtered, theEntry)
end
end
return filtered
end
function convoy.filterConvoysByRunning(theList)
-- filters all zones that have a running convoy
local filtered = {}
for idx, theZone in pairs(theList) do -- iterate all zones
local pass = true
local coa = theZone.coa -- not getCoalition!
for name, entry in pairs(convoy.convoys) do
if entry.coa == coa and entry.origin == theZone then
pass = false
end
end
if pass then
table.insert(filtered, theZone)
else
end
end
return filtered
end
function convoy.getSafeConvoyForCoa(coa, allowNeutral, maxDist)
local allMyConvoys = convoy.collectConvoysFor(coa)
local safeConvoys = convoy.sourceAndDestinationForCoa(allMyConvoys, coa, allowNeutral)
if convoy.verbose then
trigger.action.outText("+++safe convoy scan for <" .. coa .. "> returns <" .. #safeConvoys .. "> hits out of <" .. #allMyConvoys .. "> potentials:", 30)
for idx, theSol in pairs(safeConvoys) do
trigger.action.outText("zone <" .. theSol.theZone.name .. ">, group <" .. theSol.gName .. ">", 30)
end
end
if maxDist then
safeConvoys = convoy.filterConvoysByDistance(safeConvoys, maxDist)
end
if #safeConvoys < 1 then
return nil
end
local sol = dcsCommon.pickRandom(safeConvoys)
return sol.theZone, sol.gName
end
function convoy.runningForCoa(coa)
local count = 0
for name, entry in pairs(convoy.convoys) do
if entry.coa == coa then count = count + 1 end
end
return count
end
--
-- Event & Comms
--
function convoy:onEvent(theEvent)
if not theEvent then return end
if not theEvent.initiator then return end
local theUnit = theEvent.initiator
if not theUnit.getName then return end
if not theUnit.getGroup then return end
if not theUnit.getPlayerName then return end
if not theUnit:getPlayerName() then return end
local ID = theEvent.id
if ID == 15 then -- birth
convoy.installComms(theUnit)
end
end
function convoy.installComms(theUnit)
if not convoy.hasGUI then return end
if not theUnit then return end
local theGroup = theUnit:getGroup()
local gID = theGroup:getID()
local gName = theGroup:getName()
-- remove old group menu
if convoy.roots[gName] then
missionCommands.removeItemForGroup(gID, convoy.roots[gName])
end
-- handle main menu
local mainMenu = nil
if convoy.mainMenu then
mainMenu = radioMenu.getMainMenuFor(convoy.mainMenu)
end
local root = missionCommands.addSubMenuForGroup(gID, convoy.menuName, mainMenu)
convoy.roots[gName] = root
args = {}
args.theUnit = theUnit
args.gID = gID
args.gName = gName
args.coa = theGroup:getCoalition()
-- now add the submenus for convoys
local m = missionCommands.addCommandForGroup(gID, "List known Convoys", root, convoy.redirectListConvoys, args)
end
function convoy.redirectListConvoys(args)
timer.scheduleFunction(convoy.doListConvoys, args, timer.getTime() + 0.1)
end
function convoy.doListConvoys(args)
-- trigger.action.outText("enter doListConvoys", 30)
local mine = {}
local neutrals = {}
local enemy = {}
local mySide = args.coa
local gID = args.gID
-- now iterate all convoys, and sort them into bags
for convName, theConvoy in pairs (convoy.convoys) do
if theConvoy.coa == mySide then
table.insert(mine, theConvoy)
elseif theConvoy.coa == 0 then
table.insert(neutrals, theConvoy) -- note: no neutral players
else
table.insert(enemy, theConvoy)
end
end
-- we now can count each by entry num
-- build report
local msg = ""
local hasMsg = false
if convoy.listOwn and #mine > 0 then
-- report my own convoys with location
hasMsg = true
msg = msg .. "\nRUNNING ALLIED CONVOYS:\n"
for idx, theConvoy in pairs(mine) do
-- access first group from dict, there is only one
local theGroup = dcsCommon.getFirstItem(theConvoy.groups)
if theGroup and Group.isExist(theGroup) and dcsCommon.getFirstLivingUnit(theGroup) then
local theUnit = dcsCommon.getFirstLivingUnit(theGroup)
msg = msg .. " " .. theConvoy.name .. " enroute to " .. theConvoy.dest
local p = theUnit:getPoint()
local locName, hasLoc = convoy.getLocName(p)
if hasLoc then
msg = msg .. ", now some " .. locName
end
msg = msg .. "\n"
else
msg = msg .. " Lost contact with " .. theConvoy.name .. "\n"
end
end
end
if convoy.listEnemy and #enemy > 0 then
hasMsg = true
msg = msg .. "\nKNOWN/REPORTED ENEMY CONVOYS:\n"
-- enemy convoys always show closest destObject as destination!
for idx, theConvoy in pairs(enemy) do
local theGroup = dcsCommon.getFirstItem(theConvoy.groups)
if theGroup and Group.isExist(theGroup) then
msg = msg .. " " .. theConvoy.anon .. " enroute to " .. theConvoy.destObject:getName()
if theConvoy.wasAttacked then
local remU = theGroup:getUnits()
msg = msg .. ", " .. #remU .. " units remaining"
end
msg = msg .. ".\n"
if theConvoy.currWP == #theConvoy.waypoints -1 then
msg = msg .. " -=CLOSE TO DESTINATION=-\n"
end
end
end
end
if convoy.listNeutral and #neutrals > 0 then
hasMsg = true
msg = msg .. "\nKNOWN NEUTRAL CONVOYS:\n"
-- enemy convoys always show closest destObject as destination!
for idx, theConvoy in pairs(neutrals) do
local theGroup = dcsCommon.getFirstItem(theConvoy.groups)
if theGroup and Group.isExist(theGroup) then
msg = msg .. " " .. theConvoy.name .. " enroute to " .. theConvoy.destObject:getName() .. "\n"
end
end
end
if not hasMsg then
msg = "\nNO CONVOYS.\n"
end
trigger.action.outTextForGroup(gID, msg, 30)
trigger.action.outSoundForGroup(gID, convoy.actionSound)
end
--
-- UPDATE
--
function convoy.update()
timer.scheduleFunction(convoy.update, {}, timer.getTime() + 1/convoy.ups)
-- check for flags
for idx, theZone in pairs (convoy.zones) do
if theZone.spawnFlag and
theZone:testZoneFlag(theZone.spawnFlag, theZone.convoyTriggerMethod, "lastSpawnFlag") then
convoy.startConvoy(theZone)
end
end
end
function convoy.statusUpdate() -- every 10 seconds
timer.scheduleFunction(convoy.statusUpdate, {}, timer.getTime() + 10)
local redNum = 0
local blueNum = 0
local neutralNum = 0
local now = timer.getTime()
local filtered = {}
for convName, theConvoy in pairs (convoy.convoys) do
local hasLosses = false
local groupDead = false
local theZone = theConvoy.origin
local damagedGroup = nil
for gName, theGroup in pairs(theConvoy.groups) do
if Group.isExist(theGroup) then
local newNum = theGroup:getSize()
if newNum < theConvoy.groupSizes[gName] then
hasLosses = true
damagedGroup = theGroup
theConvoy.groupSizes[gName] = newNum
end
if newNum < 1 then
groupDead = true
hasLosses = false
end
else
groupDead = true
end
end
if hasLosses then
theConvoy.wasAttacked = true
if (now - theConvoy.lastAttackReport) > 300 then -- min 5 minutes between Alerts
if theZone.attackWarnings and damagedGroup then
local theUnit = dcsCommon.getFirstLivingUnit(damagedGroup)
local p = theUnit:getPoint()
local locName, hasLoc = convoy.getLocName(p)
local msg = "Convoy " .. convName .. ", enroute to destination " .. theConvoy.dest .. ", under attack"
if hasLoc then
msg = msg .. " some " .. locName
end
msg = msg .. ", taking losses."
trigger.action.outTextForCoalition(theConvoy.coa, msg, 30)
trigger.action.outSoundForCoalition(theConvoy.coa, convoy.actionSound)
end
theConvoy.lastAttackReport = now
end
convoy.invokeAttackedCallbacks(theConvoy)
theZone = theConvoy.origin
if theZone.attackedOut then
theZone:pollFlag(theZone.attackedOut, theZone.convoyMethod) -- "inc")
end
end
if groupDead then
-- invoke callback
convoy.invokeDestroyedCallbacks(theConvoy)
theZone = theConvoy.origin
if theZone.deadOut then
theZone:pollFlag(theZone.deadOut, theZone.convoyMethod) -- "inc")
end
if convoy.listOwn then
trigger.action.outTextForCoalition(theConvoy.coa, "Convoy " .. convName .. " enroute to " .. theConvoy.dest .. " was destroyed.", 30)
trigger.action.outSoundForCoalition(theConvoy.coa, convoy.actionSound)
end
if convoy.listEnemy then
local enemy = 1
if theConvoy.coa == 1 then enemy = 2 end
local msg = "Enemy convoy " .. theConvoy.anon .. " to " .. theConvoy.destObject:getName() .. " destroyed."
trigger.action.outTextForCoalition(enemy, msg, 30)
trigger.action.outSoundForCoalition(enemy, convoy.actionSound)
end
-- we deallocate after a delay, applies to helos
timer.scheduleFunction(convoy.wipeConvoy, theConvoy, now + theZone.killWipeDelay)
--end
-- do not propagate to filtered
if convoy.verbose then
trigger.action.outText("+++cnvy: filtered <" .. convName .. "> from <" .. theConvoy.origin.name .. "> to <" .. theConvoy.dest .. ">: destroyed", 30)
end
else
-- transfer for next round
if theConvoy.coa == 0 then
neutralNum = neutralNum + 1
elseif theConvoy.coa == 1 then
redNum = redNum + 1
else
blueNum = blueNum + 1
end
filtered[convName] = theConvoy
end
end
convoy.convoys = filtered
if convoy.redConvoy then
cfxZones.setFlagValue(convoy.redConvoy, redNum, convoy)
end
if convoy.blueConvoy then
cfxZones.setFlagValue(convoy.blueConvoy, blueNum, convoy)
end
if convoy.neutralConvoy then
cfxZones.setFlagValue(convoy.neutralConvoy, neutralNum, convoy)
end
if convoy.allConvoy then
cfxZones.setFlagValue(convoy.neutralConvoy, neutralNum + redNum + blueNum, convoy)
end
end
--
-- START
--
function convoy.readConfigZone()
convoy.name = "convoyConfig" -- make compatible with dml zones
local theZone = cfxZones.getZoneByName("convoyConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("convoyConfig")
end
convoy.actionSound = theZone:getStringFromZoneProperty("actionSound", "UI_SCI-FI_Tone_Bright_Dry_25_stereo.wav")
convoy.verbose = theZone.verbose
convoy.ups = theZone:getNumberFromZoneProperty("ups", 1)
convoy.menuName = theZone:getStringFromZoneProperty("menuName", "Convoys")
convoy.hasGUI = theZone:getBoolFromZoneProperty("hasGUI", true)
convoy.listEnemy = theZone:getBoolFromZoneProperty("listEnemy", true)
convoy.listNeutral = theZone:getBoolFromZoneProperty("listNeutral", true)
convoy.listOwn = theZone:getBoolFromZoneProperty("listOwn", true)
if theZone:hasProperty("attachTo:") then
local attachTo = theZone:getStringFromZoneProperty("attachTo:", "<none>")
if radioMenu then -- requires optional radio menu to have loaded
local mainMenu = radioMenu.mainMenus[attachTo]
if mainMenu then
convoy.mainMenu = mainMenu
else
trigger.action.outText("+++convoy: cannot find super menu <" .. attachTo .. ">", 30)
end
else
trigger.action.outText("+++convoy: REQUIRES radioMenu to run before convoy. 'AttachTo:' ignored.", 30)
end
end
if theZone:hasProperty("redConvoy#") then
convoy.redConvoy = theZone:getStringFromZoneProperty("redConvoy#")
end
if theZone:hasProperty("blueConvoy#") then
convoy.blueConvoy = theZone:getStringFromZoneProperty("blueConvoy#")
end
if theZone:hasProperty("neutralConvoy#") then
convoy.neutralConvoy = theZone:getStringFromZoneProperty("neutralConvoy#")
end
if theZone:hasProperty("allConvoy#") then
convoy.allConvoy = theZone:getStringFromZoneProperty("allConvoy#")
end
end
function convoy.start()
if not dcsCommon.libCheck then
trigger.action.outText("cfx convoy requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx convoy", convoy.requiredLibs) then
return false
end
-- read config
convoy.readConfigZone()
-- process convoy Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("convoy")
for k, aZone in pairs(attrZones) do
convoy.readConvoyZone(aZone) -- process attributes
convoy.addConvoyZone(aZone) -- add to list
end
-- connect event handler
world.addEventHandler(convoy)
-- start update
timer.scheduleFunction(convoy.update, {}, timer.getTime() + 1/convoy.ups)
convoy.statusUpdate()
-- start all zones that have onstart
for gName, theZone in pairs(convoy.zones) do
if theZone.onStart then
convoy.startConvoy(theZone)
end
end
-- say Hi!
trigger.action.outText("cf/x Convoy v" .. convoy.version .. " started.", 30)
return true
end
if not convoy.start() then
trigger.action.outText("convoy failed to start up")
convoy = nil
end
--[[--
convoy module
place over a fully configured group, will clone on command (start?)
reportWaypoint option. Add small script to each and every waypoint, will create report
destinationReached! -- adds script to last waypoint to hit this signal, also inits cb
dead! signal and cb. only applies to ground troops? can they disembark troops when hit?
attacked signal each time a unit is destroyed
importantType - type that must survive=
coalition / masterOwner tie-in
doWipe? to wipe all my convoys?
tacTypes = desinate units types that must survive. Upon start, ensure that at least one tac type is pressenr
when arriving, verify that it still is, or fail earlier when all tactypes are destroyed.
csar integration. when losing a vehicle, pSCAR match (say, 50%), whole convoy pushes a HOLD order to defend until time runs out or csar mission picks up evacuee (pop task).
do:
?mark source and dest of convoy on map for same side
make routes interchangeable between convoys?
make inf units disembark when convoy attacked
--]]--