mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
1079 lines
34 KiB
Lua
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
|
|
--]]-- |