------------------------------------------------------------------
-- VEAF spawn command and functions for DCS World
-- By zip (2018)
--
-- Features:
-- ---------
-- * Listen to marker change events and execute spawn commands, with optional parameters
-- * Possibilities : 
-- *    - spawn a specific ennemy unit or group
-- *    - create a cargo drop to be picked by a helo
--
-- See the documentation : https://veaf.github.io/documentation/
------------------------------------------------------------------

--- veafSpawn Table.
veafSpawn = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global settings. Stores the script constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafSpawn.Id = "SPAWN"

--- Version.
veafSpawn.Version = "1.59.0"

-- trace level, specific to this module
--veafSpawn.LogLevel = "trace"

veaf.loggers.new(veafSpawn.Id, veafSpawn.LogLevel)

--- Key phrase to look for in the mark text which triggers the spawn command.
veafSpawn.SpawnKeyphrase = "_spawn"

--- Key phrase to look for in the mark text which triggers the destroy command.
veafSpawn.DestroyKeyphrase = "_destroy"

--- Key phrase to look for in the mark text which triggers the teleport command.
veafSpawn.TeleportKeyphrase = "_teleport"

--- Key phrase to look for in the mark text which triggers the drawing commands.
veafSpawn.DrawingKeyphrase = "_drawing"

--- Key phrase to look for in the mark text which triggers the mission master commands.
veafSpawn.MissionMasterKeyphrase = "_mm"


--- Illumination flare default initial altitude (in meters AGL)
veafSpawn.IlluminationFlareAglAltitude = 1000

veafSpawn.RadioMenuName = "SPAWN"
veafSpawn.HideRadioMenu = false

--- static object type spawned when using the "logistic" keyword
veafSpawn.LogisticUnitType = "FARP Ammo Dump Coating"
veafSpawn.LogisticUnitCategory = "Fortifications"

veafSpawn.ShellingInterval = 5 -- seconds between shells, randomized by 30%
veafSpawn.FlakingInterval = 2 -- seconds between flak shells, randomized by 30%
veafSpawn.IlluminationShellingInterval = 45 -- seconds between illumination shells, randomized by 30%

veafSpawn.MIN_REPEAT_DELAY = 5

veafSpawn.HoundElintAddDelay = 1 --delay before attempting to add a unit to Hound Elint, required for aircrafts spawned dynamically at least

veafSpawn.AirUnitTemplatesPrefix = "veafSpawn-"

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Do not change anything below unless you know what you are doing!
-------------------------------------------------------------------------------------------------------------------------------------------------------------

veafSpawn.rootPath = nil

-- counts the units generated 
veafSpawn.spawnedUnitsCounter = 0

-- store all the convoys spawned
veafSpawn.spawnedConvoys = {}

-- store all the air units templates (groups, actually)
veafSpawn.airUnitTemplates = {}

-- all the named groups that have been spawned
veafSpawn.spawnedNamesIndex = {}

-- time delay between the watchdog checks for each CAP
veafSpawn.CAP_WATCHDOG_DELAY = 10

-- range scale of cargo weight biases
veafSpawn.cargoWeightBiasRange = 6

--AFAC related base data
veafSpawn.AFAC = {}
-- number of AFAC spawned
veafSpawn.AFAC.numberSpawned = {}
veafSpawn.AFAC.numberSpawned[coalition.side.BLUE] = nil
veafSpawn.AFAC.numberSpawned[coalition.side.RED] = nil
-- maximum number of AFACs allowed for spawning by players
veafSpawn.AFAC.maximumAmount = 8
-- base frequency for the first AFAC spawned
veafSpawn.AFAC.baseAFACfrequency = {}
veafSpawn.AFAC.baseAFACfrequency[coalition.side.BLUE] = 226300000 -- 226.300000 MHz otherwise known as 226300000 Hz
veafSpawn.AFAC.baseAFACfrequency[coalition.side.RED] = 226300000 -- 226.300000 MHz otherwise known as 226300000 Hz
-- callsign list of the AFACs
veafSpawn.AFAC.callsigns = {}
veafSpawn.AFAC.callsigns[coalition.side.BLUE] = {
    [1] = {name = "Enfield 9 1", taken = false},
    [2] = {name = "Springfield 9 1", taken = false},
    [3] = {name = "Uzi 9 1", taken = false},
    [4] = {name = "Colt 9 1", taken = false},
    [5] = {name = "Dodge 9 1", taken = false},
    [6] = {name = "Ford 9 1", taken = false},
    [7] = {name = "Chevy 9 1", taken = false},
    [8] = {name = "Pontiac 9 1", taken = false},
}
veafSpawn.AFAC.callsigns[coalition.side.RED] = {
    [1] = {name = "181", taken = false},
    [2] = {name = "281", taken = false},
    [3] = {name = "381", taken = false},
    [4] = {name = "481", taken = false},
    [5] = {name = "581", taken = false},
    [6] = {name = "681", taken = false},
    [7] = {name = "781", taken = false},
    [8] = {name = "881", taken = false},
}
-- AFAC mission data as MIST isn't able to recover it from dynamically spawned aircrafts
veafSpawn.AFAC.missionData = {}
veafSpawn.AFAC.missionData[coalition.side.BLUE] = {}
veafSpawn.AFAC.missionData[coalition.side.RED] = {}

veafSpawn.traceMarkerId = 3727

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Utility methods
-------------------------------------------------------------------------------------------------------------------------------------------------------------


-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event handler functions.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function executed when a mark has changed. This happens when text is entered or changed.
function veafSpawn.onEventMarkChange(eventPos, event)
    veaf.loggers.get(veafSpawn.Id):trace(string.format("event  = %s", veaf.p(event)))

    -- choose by default the coalition opposing the player who triggered the event
    local invertedCoalition = 1
    if event.coalition == 1 then
        invertedCoalition = 2
    end

    veaf.loggers.get(veafSpawn.Id):trace(string.format("event.idx  = %s", veaf.p(event.idx)))

    if veafSpawn.executeCommand(eventPos, event.text, invertedCoalition, event.idx, nil, nil, nil, nil, nil, true) then

        -- Delete old mark.
        veaf.loggers.get(veafSpawn.Id):trace(string.format("Removing mark # %d.", event.idx))
        trigger.action.removeMark(event.idx)

    end
end

function veafSpawn.executeCommand(eventPos, eventText, coalition, markId, bypassSecurity, spawnedGroups, repeatCount, repeatDelay, route, allowStartDelay)
    veaf.loggers.get(veafSpawn.Id):trace("eventPos=%s", eventPos)
    veaf.loggers.get(veafSpawn.Id):debug("eventText=%s", eventText)
    veaf.loggers.get(veafSpawn.Id):trace("coalition=%s", coalition)
    veaf.loggers.get(veafSpawn.Id):trace("markId=%s", markId)
    veaf.loggers.get(veafSpawn.Id):trace("bypassSecurity=%s", bypassSecurity)
    veaf.loggers.get(veafSpawn.Id):trace("repeatCount=%s", repeatCount)
    veaf.loggers.get(veafSpawn.Id):trace("repeatDelay=%s", repeatDelay)
    veaf.loggers.get(veafSpawn.Id):trace("route=%s", route)
    veaf.loggers.get(veafSpawn.Id):trace("allowStartDelay=%s", allowStartDelay)

    -- Check if marker has a text and the veafSpawn.SpawnKeyphrase keyphrase.
    if eventText ~= nil and (eventText:lower():find(veafSpawn.SpawnKeyphrase) or eventText:lower():find(veafSpawn.DestroyKeyphrase) or eventText:lower():find(veafSpawn.TeleportKeyphrase) or eventText:lower():find(veafSpawn.DrawingKeyphrase) or eventText:lower():find(veafSpawn.MissionMasterKeyphrase)) then

        -- Analyse the mark point text and extract the keywords.
        local options = veafSpawn.markTextAnalysis(eventText)

        if options then
            local repeatDelay = repeatDelay
            local repeatCount = repeatCount
            local allowStartDelay = allowStartDelay or false
            local startDelay = options.delayedStart

            if allowStartDelay and startDelay and startDelay > 0 then
                veaf.loggers.get(veafSpawn.Id):trace(string.format("scheduling veafSpawn.executeCommand for a delayed start in %s seconds", veaf.p(startDelay)))
                mist.scheduleFunction(veafSpawn.executeCommand, {eventPos, eventText, coalition, markId, bypassSecurity, spawnedGroups, nil, nil, route, false}, timer.getTime() + startDelay)
                return true
            end

            if options.repeatCount and not repeatCount then -- only use the parsed repeat options IF the parameter is not set (not during a repeat loop)
                -- set repeatCount and repeatDelay using the parsed options
                repeatCount = options.repeatCount
                repeatDelay = options.repeatDelay or veafSpawn.MIN_REPEAT_DELAY
                veaf.loggers.get(veafSpawn.Id):trace(string.format("using parsed repeat options to set repeatCount to %s and repeatDelay to %s", veaf.p(repeatCount), veaf.p(repeatDelay)))
            end

            if repeatCount and repeatCount > 0 then
                repeatDelay = repeatDelay
                if repeatDelay < veafSpawn.MIN_REPEAT_DELAY then
                    repeatDelay = veafSpawn.MIN_REPEAT_DELAY
                end
                repeatCount = repeatCount - 1

                -- schedule the next step of the repeated command
                veaf.loggers.get(veafSpawn.Id):trace(string.format("scheduling veafSpawn.executeCommand for %s repeats in %s seconds", veaf.p(repeatCount), veaf.p(repeatDelay)))
                mist.scheduleFunction(veafSpawn.executeCommand, {eventPos, eventText, coalition, markId, bypassSecurity, spawnedGroups, repeatCount, repeatDelay, route, false}, timer.getTime() + repeatDelay)
            end

            if not(options.radius) then
                if options.farp or options.cargo or options.logistic or options.destroy or options.teleport or options.bomb or options.smoke or options.flare or options.signal then
                    options.radius = 0
                else
                    options.radius = 150
                end
            end

            for i=1,options.multiplier do
                local spawnedGroup = nil

                if not options.side then
                    if options.country then
                        -- deduct the side from the country
                        options.side = veaf.getCoalitionForCountry(options.country, true)
                    else
                        options.side = coalition
                    end
                end

                if not options.country then
                    -- deduct the country from the side
                    options.country = veaf.getCountryForCoalition(options.side)
                end

                veaf.loggers.get(veafSpawn.Id):trace(string.format("options.side=%s",tostring(options.side)))
                veaf.loggers.get(veafSpawn.Id):trace(string.format("options.country=%s",tostring(options.country)))

                local routeDone = false

                --indication is the spawn is meant to be a convoy, to adapt it's spawning pattern
                local hasDest = false
                if (options.destination ~= nil) then
                    hasDest = true
                end

                -- Check options commands
                if options.unit then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    local code = options.laserCode
                    local channel = options.freq
                    local band = options.mod
                    if options.role == "tacan" then
                        channel = options.tacanChannel or 99
                        code = options.tacanCode or ("T"..tostring(channel))
                        band = options.tacanBand or "X"
                    end
                    spawnedGroup = veafSpawn.spawnUnit(eventPos, options.radius, options.name, options.czName, options.country, options.altitude, options.heading, options.unitName, options.role, options.forceStatic, code, channel, band, bypassSecurity, not options.showMFD)
                elseif options.farp then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    if not options.type then
                        options.type = "invisible"
                    end
                    local channel = options.tacanChannel
                    local code = options.tacanCode
                    local mod = options.tacanBand
                    spawnedGroup = veafSpawn.spawnFarp(eventPos, options.radius, options.name, options.country, options.type, options.side, options.heading, options.spacing, bypassSecurity, not options.showMFD, options.noFarpMarkers, code, channel, mod)
                elseif options.fob then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnFob(eventPos, options.radius, options.name, options.country, options.type, options.side, options.heading, options.spacing, bypassSecurity, not options.showMFD)
                elseif options.cap then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnCombatAirPatrol(eventPos, options.radius, options.name, options.country, options.altitude, options.altitudedelta, options.heading, options.distance, options.speed, options.capradius, options.skill, bypassSecurity, options.showMFD)
                elseif options.afac then
                    --check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnAFAC(eventPos, options.name, options.country, options.altitude, options.speed, options.heading, options.freq, options.mod, options.laserCode, options.immortal, false, options.showMFD)
                elseif options.group then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnGroup(eventPos, options.radius, options.name, options.czName, options.country, options.altitude, options.heading, options.spacing, options.unitName, bypassSecurity, hasDest, not options.showMFD)
                elseif options.infantryGroup then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnInfantryGroup(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.armor, options.size, bypassSecurity, not options.showMFD)
                elseif options.armoredPlatoon then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnArmoredPlatoon(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.armor, options.size, bypassSecurity, hasDest, not options.showMFD)
                elseif options.airDefenseBattery then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnAirDefenseBattery(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, bypassSecurity, hasDest, not options.showMFD)
                elseif options.transportCompany then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnTransportCompany(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.size, bypassSecurity, hasDest, not options.showMFD)
                elseif options.fullCombatGroup then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnFullCombatGroup(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.armor, options.size, bypassSecurity, not options.showMFD)
                elseif options.convoy then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnConvoy(eventPos, options.name, options.czName, options.radius, options.country, options.side, options.heading, options.spacing, options.speed, options.patrol, options.offroad, options.destination, options.defense, options.size, options.armor, bypassSecurity, not options.showMFD)
                    routeDone = true
                elseif options.cargo then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnCargo(eventPos, options.radius, options.cargoType, options.country, options.cargoWeightBias, options.cargoSmoke, options.unitName, bypassSecurity, not options.showMFD)
                elseif options.logistic then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end
                    spawnedGroup = veafSpawn.spawnLogistic(eventPos, options.radius, options.country, bypassSecurity, not options.showMFD)
                elseif options.destroy then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end
                    veafSpawn.destroy(eventPos, options.radius, options.unitName)
                elseif options.teleport then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end
                    veafSpawn.teleport(eventPos, options.name, bypassSecurity)
                elseif options.bomb then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end
                    veafSpawn.spawnBomb(eventPos, options.radius, options.shells, options.power, options.altitude, options.altitudedelta, options.password)
                elseif options.smoke then
                    veafSpawn.spawnSmoke(eventPos, options.smokeColor, options.radius, options.shells)
                elseif options.flare then
                    if not options.altitude or options.altitude == 0 then
                        options.altitude = 1000
                    end
                    if not options.power or options.power == 0 then
                        options.power = 500
                    end
                    options.power = options.power * 1000
                    veafSpawn.spawnIlluminationFlare(eventPos, options.radius, options.shells, options.power, options.altitude, options.heading, options.distance, options.speed)
                elseif options.signal then
                    veafSpawn.spawnSignalFlare(eventPos, options.radius, options.shells, options.smokeColor)
                elseif options.addDrawing then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end
                    veafSpawn.addPointToDrawing(eventPos, options.name, options.drawColor, options.drawFillColor, options.type, options.drawArrow)
                elseif options.drawSquare then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end
                    veafSpawn.drawSquare(eventPos, options.name, options.radius, options.drawColor, options.drawFillColor, options.type)
                elseif options.drawCircle then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end
                    veafSpawn.drawCircle(eventPos, options.name, options.radius, options.drawColor, options.drawFillColor, options.type)
                elseif options.eraseDrawing then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end
                    veafSpawn.eraseDrawing(options.name)
                elseif options.mmFlagOn then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end
                    veafSpawn.missionMasterSetFlag(options.name, 1)
                elseif options.mmFlagOff then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end
                    veafSpawn.missionMasterSetFlag(options.name, 0)
                elseif options.mmGetFlag then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end
                    veafSpawn.missionMasterGetFlag(options.name)
                elseif options.mmRun then
                    -- check security
                    if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end
                    veafSpawn.missionMasterRun(options.name)
                end
                if spawnedGroup then
                    local groupObject = Group.getByName(spawnedGroup)
                    local isStatic = false
                    --group might not have been found because it was a static
                    if not groupObject then
                        isStatic = true
                        groupObject = StaticObject.getByName(spawnedGroup)
                    end
                    veaf.loggers.get(veafSpawn.Id):trace("got groupObject (isStatic=%s) to add group to other platforms : %s", veaf.p(isStatic), veaf.p(groupObject))
                    if groupObject then
                        if not isStatic then
                            --stuff below does not support statics
                            -- make the group combat ready ! well except if the user said otherwise, tweak the AlarmState for some scenarios
                            --veaf.loggers.get(veafSpawn.Id):trace("options.disperse=%s", veaf.p(options.disperse))
                            veaf.readyForCombat(groupObject, options.AlarmState, options.disperse)
                            if not route and not routeDone and options.destination then
                                --  make the group go to destination
                                local actualPosition = groupObject:getUnit(1):getPosition().p
                                local route = veaf.generateVehiclesRoute(actualPosition, options.destination, not options.offroad, options.speed, options.patrol, spawnedGroup)
                                mist.goRoute(groupObject, route)
                            elseif route then
                                mist.goRoute(groupObject, route)
                            end
                            -- add the group to the IADS, if there is one
                            if veafSkynet and not veafSkynet.DynamicSpawn and options.skynet then -- only add static stuff like sam groups and sam batteries, not mobile groups and convoys -- and do not do that if DynamicSpawn is active in VeafSkynet
                                veaf.loggers.get(veafSpawn.Id):trace("options.skynet= %s", veaf.p(options.skynet))
                                if type(options.skynet) == "boolean" then --it means options.skynet is true
                                    options.skynet = veafSkynet.defaultIADS[tostring(options.side)]
                                end
                                veaf.loggers.get(veafSpawn.Id):trace("Adding spawned group to skynet, networkName= %s", veaf.p(options.skynet))
                                local networkName = options.skynet
                                if veafSkynet.addGroupToNetwork(networkName, groupObject, options.forceEwr, options.pointDefense, nil, bypassSecurity) then
                                    veaf.loggers.get(veafSpawn.Id):trace("Group Added to IADS network")
                                    if not bypassSecurity then trigger.action.outText(string.format("Group added to the IADS named \"%s\"", options.skynet),15) end
                                else
                                    veaf.loggers.get(veafSpawn.Id):trace("Could not find IADS network or group is not supported by IADS")
                                    if not bypassSecurity then trigger.action.outText(string.format("Could not add group to the IADS named \"%s\", network not found or group not supported", options.skynet),15) end
                                end
                            end
                        end
                        --but houndElint for example does support statics
                        -- reset the Hound Elint system, if the module is active
                        if veafHoundElint then
                            mist.scheduleFunction(veafHoundElint.addPlatformToSystem, {groupObject, nil, false}, timer.getTime()+veafSpawn.HoundElintAddDelay)
                        end
                        --might need to specify the if a group was static in here so that people on the other end know
                        if spawnedGroups then
                            table.insert(spawnedGroups, spawnedGroup)
                        end
                    end
                end
            end
            return true
        end
    end
    return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Analyse the mark text and extract keywords.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

function veafSpawn.convertLaserToFreq(laser)
    veaf.loggers.get(veafSpawn.Id):trace(string.format("convertLaserToFreq(laser=%s)", tostring(laser)))
    local laser = tonumber(laser)
    if laser and laser >= 1111 and laser <= 1688 then
        local laserB = math.floor((laser - 1000)/100)
        local laserCD = laser - 1000 - laserB*100
        local frequency = tostring(30+laserB+laserCD*0.05)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("laserB=%s", tostring(laserB)))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("laserCD=%s", tostring(laserCD)))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("frequency=%s", tostring(frequency)))
        return frequency
    else
        return nil
    end
end

--- Extract keywords from mark text.
function veafSpawn.markTextAnalysis(text)
    veaf.loggers.get(veafSpawn.Id):trace(string.format("veafSpawn.markTextAnalysis(text=%s)", text))


    -- Option parameters extracted from the mark text.
    local options = {}
    options.czName = nil -- name of the CZ to add to the group name, if any
    options.unit = false
    options.forceStatic = false -- if true, will force the spawned unit to be a static
    options.group = false
    options.cap = false
    options.farp = false
    options.noFarpMarkers = false
    options.fob = false
    options.type = nil
    options.cargo = false
    options.logistic = false
    options.smoke = false
    options.flare = false
    options.signal = false
    options.bomb = false
    options.destroy = false
    options.teleport = false
    options.convoy = false
    options.role = nil
    options.laserCode = 1688
    options.infantryGroup = false
    options.armoredPlatoon = false
    options.airDefenseBattery = false
    options.transportCompany = false
    options.fullCombatGroup = false
    options.speed = nil
    options.capradius = nil
    options.shells = 1
    options.multiplier = 1
    options.skynet = false -- if true, add to skynet
    options.forceEwr = false -- if true, unit will be added as an IADS EWR
    options.pointDefense = false -- if true, unit will be added as point defense to the closest IADS SAM site
    options.AlarmState = 2 -- Alarm state of the convoy to be spawned, 0 is AUTO, 1 is GREEN, 2 is RED. Note: This option is useful for some vehicules which behave badly in Alarm State RED when spawned such as the Scud or Sa-11 (they deploy and can't drive anywhere). Auto is better suited
    options.disperse = 15 --disperse time of groups if under attack, by default is set to 20s
    options.showMFD = false --option to enable groups to be seen on MFDs
    options.addDrawing = false -- draw a polygon on the map
    options.drawSquare = false -- draw a square on the map
    options.drawCircle = false -- draw a circle on the map
    options.eraseDrawing = false -- erase a polygon from the map
    options.stopDrawing = false -- close a polygon started on the map

    options.drawColor = nil
    options.drawFillColor = nil
    options.drawArrow = nil

    -- spawned group/unit type/alias
    options.name = ""

    -- spawned unit name
    options.unitName = nil

    -- spawned group units spacing
    options.spacing = 5

    options.country = nil
    options.side = nil
    options.altitude = 0
    options.altitudedelta = 0
    options.heading = 0
    options.distance = nil
    options.skill = nil

    -- if true, group is part of a road convoy
    options.isConvoy = false

    -- if true, group is patroling between its spawn point and its destination named point
    options.patrol = false

    -- if true, group is set to not follow roads
    options.offroad = false

    -- if set and convoy is true, send the group to the named point
    options.destination = nil

    -- the size of the generated dynamic groups (platoons, convoys, etc.)
    options.size = math.random(7) + 8

    -- defenses force ; ranges from 1 to 5, 5 being the toughest.
    options.defense = math.random(5)

    -- armor force ; ranges from 1 to 5, 5 being the strongest and most modern.
    options.armor = math.random(5)

    -- bomb power
    options.power = 100

    -- smoke color
    options.smokeColor = trigger.smokeColor.Red

    -- optional cargo smoke
    options.cargoSmoke = false

    -- cargo type
    options.cargoType = "container_cargo"
    options.cargoWeightBias = 2 --weight bias of the cargo, if equal to 0, cargo will be very close to minimum weight, if equal to 5, cargo will be close to maximum

    options.password = nil

    --AFAC spawn option
    options.afac = false
    options.immortal = false

    -- JTAC radio comms
    options.freq = veafSpawn.convertLaserToFreq(options.laserCode)
    options.mod = "fm"

    -- TACAN name and channel
    options.tacanChannel = nil
    options.tacanBand = nil

    -- repeat options
    options.repeatCount = nil
    options.repeatDelay = nil

    -- delayed start option
    options.delayedStart = 0

    -- Check for correct keywords.
    if text:lower():find(veafSpawn.SpawnKeyphrase .. " unit") then
        options.unit = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " afac") then
        options.afac = true
        --default country for the AFAC
        options.country = "USA"
        --default AFAC spawned
        options.name = "mq-9"
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " cap") then
        options.cap = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " group") then
        options.group = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " farp") then
        options.farp = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " fob") then
        options.fob = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " convoy") then
        options.convoy = true
        options.size = 10 -- default the size parameter to 10
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " infantrygroup") then
        options.infantryGroup = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " armorgroup") then
        options.armoredPlatoon = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " samgroup") then
        options.airDefenseBattery = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " transportgroup") then
        options.transportCompany = true
        options.size = math.random(2, 5)
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " combatgroup") then
        options.fullCombatGroup = true
        options.size = 1 -- default the size parameter to 1
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " smoke") then
        options.smoke = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " flare") then
        options.flare = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " signal") then
        options.signal = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " cargo") then
        options.cargo = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " logistic") then
        options.logistic = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " bomb") then
        options.bomb = true
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " jtac") then
        options.role = 'jtac'
        options.unit = true
        -- default country for friendly JTAC: USA
        options.country = "USA"
        -- default name for JTAC
        options.name = "LUV HMMWV Jeep"
        -- default JTAC name (will overwrite previous unit with same name)
        options.unitName = "JTAC1"
    elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " tacan") then
        options.role = 'tacan'
        options.unit = true
        -- default country for friendly tacan: USA
        options.country = "USA"
        -- default name for tacan
        options.name = "TACAN_beacon"
        -- default name (will overwrite previous unit with same name)
        options.unitName = "TACAN TCN"
    elseif text:lower():find(veafSpawn.DestroyKeyphrase) then
        options.destroy = true
    elseif text:lower():find(veafSpawn.TeleportKeyphrase) then
        options.teleport = true
    elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " add") then
        options.addDrawing = true
    elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " erase") then
        options.eraseDrawing = true
    elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " square") then
        options.drawSquare = true
    elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " circle") then
        options.drawCircle = true
    elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " flagon") then
        options.mmFlagOn = true
    elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " flagoff") then
        options.mmFlagOff = true
    elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " getflag") then
        options.mmGetFlag = true
    elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " run") then
        options.mmRun = true
    else
        return nil
    end

    -- keywords are split by ","
    local keywords = veaf.split(text, ",")

    for _, keyphrase in pairs(keywords) do
        -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma.
        local str = veaf.breakString(veaf.trim(keyphrase), " ")
        local key = str[1]
        local val = str[2] or ""

        if key:lower() == "unitname" then
            -- Set name.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword unitname = %s", tostring(val)))
            options.unitName = val
        end

        if key:lower() == "name" then
            -- Set name.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword name = %s", tostring(val)))
            options.name = val
        end

        if key:lower() == "czname" then
            -- Set name.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword czname = %s", tostring(val)))
            options.czName = val
        end

        if (key:lower() == "destination" or key:lower() == "dest") then
            -- Set destination.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword destination = %s", tostring(val)))
            options.destination = val
            options.AlarmState = 0 --since some units will not move when they are told to have an alarm state red, it's best to by default leave it on auto. AI is pretty all knowing anyways, it knows when it should go to red state
            options.spacing = 1 --compress the convoy to not make it extremely long at departure
            options.radius = 1 --convoy spawns on the marker exactly to not have them spawn in trees etc.
        end

        if key:lower() == "isconvoy" then
            veaf.loggers.get(veafSpawn.Id):trace("Keyword isconvoy found")
            options.convoy = true
        end

        if key:lower() == "patrol" then
            veaf.loggers.get(veafSpawn.Id):trace("Keyword patrol found")
            options.patrol = true
        end

        if key:lower() == "offroad" then
            veaf.loggers.get(veafSpawn.Id):trace("Keyword offroad found")
            options.offroad = true
        end

        if key:lower() == "skynet" then
            -- Retreive the name of the IADS you wish to add the spawned group to
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword skynet = %s", tostring(val)))
            options.skynet = val:lower()
            if options.skynet == "" or options.skynet == "true" then
                options.skynet = true
            elseif options.skynet == "false" then
                options.skynet = false
            end
        end

        if key:lower() == "ewr" then
            -- Set force IADS EWR toggle for unit spawn
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword ewr found"))
            options.forceEwr = true
        end

        if key:lower() == "pointdefense" then
            -- Tells IADS to add the spawned SAM to the point defenses of the specified site or to the nearest site
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword pointdefense found"))
            options.pointDefense = true
            if val ~= "" then
                veaf.loggers.get(veafSpawn.Id):trace(string.format("groupName specified : %s", tostring(val)))
                options.pointDefense = tostring(val)
            end
        end

        --to be placed after the skynet input, SAMs in the skynet network work better if set to AlarmState RED, so AlarmState is equal to 2 if skynet is enabled
        if key:lower() == "alarm" then
            -- Set Alarm State of the unit to be spawned
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword alarm = %s", tostring(val)))
            if (val == "0" or val == "2" or val =="1") and not options.skynet then
                options.AlarmState = tonumber(val)
            end
        end

        if key:lower() == "radius" then
            -- Set name.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword radius = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.radius = nVal
        end

        if key:lower() == "spacing" then
            -- Set spacing.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword spacing = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.spacing = nVal
        end

        if key:lower() == "multiplier" then
            -- Set multiplier.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword multiplier = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.multiplier = nVal
        end

        if key:lower() == "alt" then
            -- Set altitude.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword alt = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.altitude = nVal
        end

        if key:lower() == "altdelta" then
            -- Set altitude delta.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword altdelta = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.altitudedelta = nVal
        end

        if key:lower() == "speed" then
            -- Set speed.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword speed = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.speed = nVal
        end

        if key:lower() == "capradius" then
            -- Set capradius.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword capradius = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.capradius = nVal
        end

        if key:lower() == "shells" then
            -- Set altitude.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword shells = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.shells = nVal
        end

        if key:lower() == "hdg" then
            -- Set heading.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword hdg = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.heading = nVal
        end

        if key:lower() == "heading" then
            -- Set heading.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword heading = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.heading = nVal
        end

        if key:lower() == "country" then
            -- Set country
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword country = %s", tostring(val)))
            options.country = val:upper()
        end

        if key:lower() == "side" then
            -- Set side
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword side = %s", tostring(val)))
            if val:upper() == "BLUE" then
                options.side = veafCasMission.SIDE_BLUE
            else
                options.side = veafCasMission.SIDE_RED
            end
        end

        if key:lower() == "password" then
            -- Unlock the command
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword password", tostring(val)))
            options.password = val
        end

        if key:lower() == "power" then
            -- Set bomb power.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword power = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.power = nVal
        end

        if key:lower() == "laser" then
            -- Set laser code.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("laser code = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.freq = veafSpawn.convertLaserToFreq(nVal)
            options.laserCode = nVal
        end

        if key:lower() == "freq" then
            -- Set JTAC/AFAC frequency.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("freq = %s", tostring(val)))
            options.freq = val
        end

        if key:lower() == "mod" then
            -- Set JTAC/AFAC modulation.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("mod = %s", tostring(val)))
            options.mod = val
        end

        if key:lower() == "band" then
            -- Set TACAN band
            veaf.loggers.get(veafSpawn.Id):trace(string.format("band = %s", tostring(val)))
            options.tacanBand = val
        end

        if key:lower() == "code" then
            -- Set TACAN code
            veaf.loggers.get(veafSpawn.Id):trace(string.format("code = %s", tostring(val)))
            options.tacanCode = val
        end

        if key:lower() == "channel" then
            -- Set TACAN channel.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("channel = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.tacanChannel = nVal
        end

        if key:lower() == "arrow" then
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword arrow = %s", tostring(val)))
            options.drawArrow = true
        end
        if key:lower() == "fill" then
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword fill = %s", tostring(val)))
            options.drawFillColor = val
        end

        if key:lower() == "color" then
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword color = %s", tostring(val)))
            options.drawColor = val
            -- Set smoke color.
            if (val:lower() == "red") then
                options.smokeColor = trigger.smokeColor.Red
            elseif (val:lower() == "green") then
                options.smokeColor = trigger.smokeColor.Green
            elseif (val:lower() == "orange") then
                options.smokeColor = trigger.smokeColor.Orange
            elseif (val:lower() == "blue") then
                options.smokeColor = trigger.smokeColor.Blue
            elseif (val:lower() == "white") then
                options.smokeColor = trigger.smokeColor.White
            end
        end

        if key:lower() == "skill" then
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword skill = %s", tostring(val)))
            options.skill = val
        end

        if key:lower() == "dist" or key:lower() == "distance" then
            -- Set distance.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword distance = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.distance = nVal
        end

        if options.cargo and key:lower() == "name" then
            -- Set cargo type.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword name = %s", tostring(val)))
            options.cargoType = val
        end

        if options.cargo and key:lower() == "weight" then
            -- Set cargo type.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword weight = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            if nVal >= 0 and nVal <= veafSpawn.cargoWeightBiasRange then
                options.cargoWeightBias = nVal
            elseif nVal > veafSpawn.cargoWeightBiasRange then
                options.cargoWeightBias = veafSpawn.cargoWeightBiasRange
            elseif nVal < 0 then
                options.cargoWeightBias = 0
            end
        end

        if key:lower() == "type" then
            -- Set farp type.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword type = %s", tostring(val)))
            options.type = val
        end

        if options.farp and key:lower() == "nofarpmarkers" then
            -- Skip the invisible FARP special vehicles that mark the position of the FARP
            veaf.loggers.get(veafSpawn.Id):trace("Keyword noFarpMarkers is set")
            options.noFarpMarkers = true
        end

        if options.cargo and key:lower() == "smoke" then
            -- Mark with green smoke.
            veaf.loggers.get(veafSpawn.Id):trace("Keyword smoke is set")
            options.cargoSmoke = true
        end

        if key:lower() == "size" then
            -- Set size.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword size = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.size = nVal
        end

        if key:lower() == "defense" then
            -- Set defense.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword defense = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            if nVal >= 0 then
                options.defense = nVal
            end
        end

        if key:lower() == "armor" then
            -- Set armor.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword armor = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            if nVal >= 0 then
                options.armor = nVal
            end
        end

        if key:lower() == "repeat" then
            -- Set repeat count.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword repeat = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.repeatCount = nVal
        end

        if key:lower() == "delay" then
            -- Set delay.
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword delay = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            options.repeatDelay = nVal
        end

        if key:lower() == "static" then
            -- Set static unit spawn toggle
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword static found"))
            options.forceStatic = true
        end

        if key:lower() == "immortal" then
            -- Set spawned unit to invisible and immortal
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword immortal found"))
            options.immortal = true
        end

        if key:lower() == "delayed" then
            -- Set delayed start on first spawn occurence
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword delayed = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            if nVal >= 0 then
                options.delayedStart = nVal
            else
                options.delayedStart = veafSpawn.MIN_REPEAT_DELAY
            end
        end

        if key:lower() == "showmfd" then
            -- Set hiddenOnMFD option or not
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword showmfd found"))
            options.showMFD = true
        end

        if key:lower() == "disperse" then
            -- Set hiddenOnMFD option or not
            veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword disperse = %s", tostring(val)))
            local nVal = veaf.getRandomizableNumeric(val)
            if nVal >= 0 then
                options.disperse = nVal
            end
        end
    end

    -- check mandatory parameter "name" for command "group"
    if options.group and not(options.name) then return nil end

    -- check mandatory parameter "name" for command "unit"
    if options.unit and not(options.name) then return nil end

    -- check mandatory parameter "name" for all mission master commands
    if (options.mmFlagOff or options.mmFlagOn or options.mmRun) and not(options.name) then return nil end

return options
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Manage drawings on the map
-------------------------------------------------------------------------------------------------------------------------------------------------------------

veafSpawn.drawings = {}
veafSpawn.drawingsMarkers = {}

--- Add a point to a drawing on the map (or start a new drawing)
function veafSpawn.addPointToDrawing(point, name, color, fillColor, lineType, isArrow)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("addPointToDrawing(point=%s, name=%s, color=%s, fillColor=%s, lineType=%s, isArrow=%s)", veaf.p(point), veaf.p(name), veaf.p(color), veaf.p(fillColor), veaf.p(lineType), veaf.p(isArrow)))
    if not name then
        veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands")
        return
    end
    local drawing = veafSpawn.drawings[name:lower()]
    if not drawing then
        drawing = VeafDrawingOnMap:new():setName(name)
        veafSpawn.drawings[name:lower()] = drawing
    end
    local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()]
    if drawingMarkerId then
        trigger.action.removeMark(drawingMarkerId)
    end
    drawingMarkerId = veaf.getUniqueIdentifier()
    trigger.action.markToAll(drawingMarkerId, name, point, true)
    veafSpawn.drawingsMarkers[name:lower()] = drawingMarkerId
    if color then
        drawing:setColor(color)
    end
    if lineType then
        drawing:setLineType(lineType)
    end
    if isArrow then
        drawing:setArrow()
    end
    if fillColor then
        drawing:setFillColor(fillColor)
    end

    drawing:addPoint(point)
    drawing:draw()
end

--- Add a circle to the map
function veafSpawn.drawCircle(point, name, radius, color, fillColor, lineType)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("drawCircle(point=%s, name=%s, radius=%s, color=%s, fillColor=%s, lineType=%s)", veaf.p(point), veaf.p(name), veaf.p(radius), veaf.p(color), veaf.p(fillColor), veaf.p(lineType)))
    if not name then
        veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands")
        return
    end
    local drawing = veafSpawn.drawings[name:lower()]
    if drawing then
        -- erase the old one first
        drawing:erase()
        veafSpawn.drawings[name:lower()] = nil
    end
    drawing = VeafCircleOnMap:new():setName(name)
    drawing:setCenter(point)
    drawing:setRadius(radius or 5000)
    veafSpawn.drawings[name:lower()] = drawing
    local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()]
    if drawingMarkerId then
        trigger.action.removeMark(drawingMarkerId)
    end
    drawingMarkerId = veaf.getUniqueIdentifier()
    trigger.action.markToAll(drawingMarkerId, name, point, true)
    veafSpawn.drawingsMarkers[name:lower()] = drawingMarkerId
    if color then
        drawing:setColor(color)
    end
    if lineType then
        drawing:setLineType(lineType)
    end
    if fillColor then
        drawing:setFillColor(fillColor)
    end
    drawing:draw()
end

--- Add a square to the map
function veafSpawn.drawSquare(point, name, side, color, fillColor, lineType)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("drawSquare(point=%s, name=%s, side=%s, color=%s, fillColor=%s, lineType=%s)", veaf.p(point), veaf.p(name), veaf.p(side), veaf.p(color), veaf.p(fillColor), veaf.p(lineType)))
    if not name then
        veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands")
        return
    end
    local drawing = veafSpawn.drawings[name:lower()]
    if drawing then
        -- erase the old one first
        drawing:erase()
        veafSpawn.drawings[name:lower()] = nil
    end
    drawing = VeafSquareOnMap:new():setName(name)
    drawing:setCenter(point)
    drawing:setSide(side or 5000)
    veafSpawn.drawings[name:lower()] = drawing
    local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()]
    if drawingMarkerId then
        trigger.action.removeMark(drawingMarkerId)
    end
    drawingMarkerId = veaf.getUniqueIdentifier()
    trigger.action.markToAll(drawingMarkerId, name, point, true)
    veafSpawn.drawingsMarkers[name:lower()] = drawingMarkerId
    if color then
        drawing:setColor(color)
    end
    if lineType then
        drawing:setLineType(lineType)
    end
    if fillColor then
        drawing:setFillColor(fillColor)
    end
    drawing:draw()
end

--- Erase drawing from the map
function veafSpawn.eraseDrawing(name)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("eraseDrawing(name=%s)",veaf.p(name)))
    if not name then
        veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands")
        return
    end
    local drawing = veafSpawn.drawings[name:lower()]
    if not drawing then
        local message = string.format("Could not find a drawing named %s", veaf.p(name))
        veaf.loggers.getSpawn(veaf.Id):warn(message)
        trigger.action.outText(message, 5)
        return
    end
    drawing:erase()
    veafSpawn.drawings[name:lower()] = nil
    local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()]
    if drawingMarkerId then
        trigger.action.removeMark(drawingMarkerId)
    end
    veafSpawn.drawingsMarkers[name:lower()] = nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Group spawn command
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Spawn a specific group at a specific spot
function veafSpawn.doSpawnGroup(spawnSpot, radius, groupDefinition, czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD, shuffle)
    veaf.loggers.get(veafSpawn.Id):debug("doSpawnGroup(czName=%s, country=%s, alt=%s, hdg=%s, spacing=%s, groupName=%s, silent=%s, hasDest=%s, hiddenOnMFD=%s, shuffle=%s)", czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD, shuffle)

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot))

    veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1

    if type(groupDefinition) == "string" then
        local name = groupDefinition
        -- find the desired group in the groups database
        groupDefinition = veafUnits.findGroup(name)
        if not(groupDefinition) then
            veaf.loggers.get(veafSpawn.Id):info("cannot find group "..name)
            if not(silent) then
                trigger.action.outText("cannot find group "..name, 5)
            end
            return nil
        end
    end

    veaf.loggers.get(veafSpawn.Id):trace("doSpawnGroup: groupDefinition.description=" .. groupDefinition.description)

    local units = {}

    -- place group units on the map
    local group, cells = veafUnits.placeGroup(groupDefinition, spawnSpot, spacing, hdg, hasDest)
    veafUnits.traceGroup(group, cells)

    if not(groupName) then
        groupName = group.groupName or "spawned group"
    end
    -- use the centralized group naming function
    groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), groupName, czName)

    if hasDest then
        mist.scheduleFunction(veafUnits.removePathfindingFixUnit,{groupName}, timer.getTime()+veafUnits.delayBeforePathfindingFix)
    end

    for i=1, #group.units do
        local unit = group.units[i]
        local unitType = unit.typeName
        local unitName = groupName .. " / " .. unit.displayName .. " #" .. i

        local spawnPoint = unit.spawnPoint
        if alt > 0 then
            spawnPoint.y = alt
        end

        -- check if position is correct for the unit type
        if not veafUnits.checkPositionForUnit(spawnPoint, unit) then
            veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning unit ".. unitType)
            if not(silent) then
                trigger.action.outText("cannot find a suitable position for spawning unit "..unitType, 5)
            end
        else
            local toInsert = {
                    ["x"] = spawnPoint.x,
                    ["y"] = spawnPoint.z,
                    ["alt"] = spawnPoint.y,
                    ["type"] = unitType,
                    ["name"] = unitName,
                    ["speed"] = 0,  -- speed in m/s
                    ["skill"] = "Random",
                    ["heading"] = spawnPoint.hdg
            }

            veaf.loggers.get(veafSpawn.Id):trace(string.format("toInsert x=%.1f y=%.1f, alt=%.1f, type=%s, name=%s, speed=%d, heading=%d, skill=%s, country=%s", veaf.p(toInsert.x), veaf.p(toInsert.y), veaf.p(toInsert.alt), veaf.p(toInsert.type), veaf.p(toInsert.name), veaf.p(toInsert.speed), veaf.p(mist.utils.toDegree(toInsert.heading)), veaf.p(toInsert.skill), veaf.p(country)))
            table.insert(units, toInsert)
        end
    end

    -- shuffle the group if needed (useful for randomizing convoys)
    -- counter productive with hasDest which to speed up convoys orders all of the units so that they spawn in order and in a line
    -- the best way to execute this shuffle is to create groups with random cells for each unit, TBD
    if shuffle and not hasDest then
        units = veaf.shuffle(units)
    end

    -- actually spawn the group
    if group.naval then
        mist.dynAdd({country = country, category = "SHIP", name = groupName, hidden = false, units = units, hiddenOnMFD = hiddenOnMFD})
    elseif group.air then
        mist.dynAdd({country = country, category = "AIRPLANE", name = groupName, hidden = false, units = units, hiddenOnMFD = hiddenOnMFD})
    else
        mist.dynAdd({country = country, category = "GROUND_UNIT", name = groupName, hidden = false, units = units, hiddenOnMFD = hiddenOnMFD})
    end

    if not(silent) then
        -- message the group spawning
        trigger.action.outText("A " .. group.description .. "("..country..") has been spawned", 5)
    end

    return groupName
end

--- Spawn a FARP
function veafSpawn.spawnFarp(spawnSpot, radius, name, country, farptype, side, hdg, spacing, silent, hiddenOnMFD, noFarpMarkers, code, freq, mod)
    veaf.loggers.get(veafSpawn.Id):debug("spawnFarp(name=%s, country=%s, farptype=%s, side=%s, hdg=%s, spacing=%s, silent=%s, hiddenOnMFD=%s, noFarpMarkers=%s)",veaf.p(name), veaf.p(country), veaf.p(farptype), veaf.p(side), veaf.p(hdg), veaf.p(spacing), veaf.p(silent), veaf.p(hiddenOnMFD), veaf.p(noFarpMarkers))

    local radius = radius or 0
    local name = name
    local hdg = hdg or 0
    local side = side or 1
    local country = country or "usa"
    local farptype = farptype or ""
    local noFarpMarkers = noFarpMarkers or false

    local spawnPosition = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnPosition=%s", veaf.p(spawnPosition))
    if not name or name == "" then
        local _lat, _lon = coord.LOtoLL(spawnSpot)
        veaf.loggers.get(veafSpawn.Id):trace("_lat=%s", veaf.p(_lat))
        veaf.loggers.get(veafSpawn.Id):trace("_lon=%s", veaf.p(_lon))
        local _mgrs = coord.LLtoMGRS(_lat, _lon)
        veaf.loggers.get(veafSpawn.Id):trace("_mgrs=%s", veaf.p(_mgrs))
        local _UTM = _mgrs.MGRSDigraph .. math.floor(_mgrs.Easting / 1000) .. math.floor(_mgrs.Northing / 1000)
        name = "FARP ".. _UTM:upper() .. "-" .. timer.getTime()
    end

    local _type = "Invisible FARP"
    local _shape = "invisiblefarp"
    if farptype:lower() == "quad" then
        _type = "FARP"
        _shape = "FARPs"
    elseif farptype:lower() == "single" then
        _type = "FARP"
        _shape = "FARP"
    elseif farptype:lower() == "pad" then
        _type = "FARP_SINGLE_01"
        _shape = "FARP_SINGLE_01"
    elseif farptype:lower() == "invisible" then
        _type = "Invisible FARP"
        _shape = "invisiblefarp"
    end

    -- spawn the FARP
    local _farpStatic = {
        ["category"] = "Heliports",
        ["shape_name"] = _shape,
        ["type"] = _type,
        ["unitId"] =  mist.getNextUnitId(),
        ["y"] = spawnPosition.z,
        ["x"] = spawnPosition.x,
        ["groupName"] = name,
        ["name"] = name,
        ["canCargo"] = false,
        ["heading"] = mist.utils.toRadian(hdg),
        ["country"] = country,
        ["coalition"] = side,
        ["dead"] = false,
        ["dynamicCargo"] = true,
        ["dynamicSpawn"] = true,
        ["allowHotStart"] = true,
        --["unlimitedAircrafts"] = true,
        ["unlimitedFuel"] = true,
        ["unlimitedMunitions"] = true,
    }
    mist.dynAddStatic(_farpStatic)
    local _spawnedFARP = StaticObject.getByName(name)
    veaf.loggers.get(veafSpawn.Id):trace("_spawnedFARP=%s", veaf.p(_spawnedFARP))

    if _spawnedFARP then
        veaf.loggers.get(veafSpawn.Id):debug("Spawned the FARP static %s", veaf.p(name))

        -- populate the FARP but make the units invisible to MFDs as they are redundant (FARP already shows if wanted)
        veafGrass.buildFarpUnits(_farpStatic, nil, name, hiddenOnMFD, noFarpMarkers, code, freq, mod)
    end

    return name
end

--- Spawn a FARP
function veafSpawn.spawnFob(spawnSpot, radius, name, country, fobtype, side, hdg, spacing, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnFob(name=%s, country=%s, fobtype=%s, side=%s, hdg=%s, spacing=%s, silent=%s, hiddenOnMFD=%s)",veaf.p(name), veaf.p(country), veaf.p(fobtype), veaf.p(side), veaf.p(hdg), veaf.p(spacing), veaf.p(silent), veaf.p(hiddenOnMFD))
    local TOWER_DISTANCE = 20
    local BEACON_DISTANCE = 3

    if not veaf.ctld_initialized then
        veaf.loggers.get(veafSpawn.Id):error("spawnFob([%s]): cannot spawn FOB without CTLD!)",veaf.p(name))
        return nil
    end

    local _radius = radius or 0
    local _fobName = name
    local _side = side or 1
    local _country = country or "usa"
    local _fobtype = fobtype or "" -- only a single FOB type in CTLD, yet
	local _hdg = hdg or 0

    local _spawnPosition = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, _radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnPosition=%s", veaf.p(_spawnPosition))
    if not _fobName or _fobName == "" then
        local _lat, _lon = coord.LOtoLL(spawnSpot)
        veaf.loggers.get(veafSpawn.Id):trace("_lat=%s", veaf.p(_lat))
        veaf.loggers.get(veafSpawn.Id):trace("_lon=%s", veaf.p(_lon))
        local _mgrs = coord.LLtoMGRS(_lat, _lon)
        veaf.loggers.get(veafSpawn.Id):trace("_mgrs=%s", veaf.p(_mgrs))
        local _UTM = _mgrs.MGRSDigraph .. math.floor(_mgrs.Easting / 1000) .. math.floor(_mgrs.Northing / 1000)
        _fobName = "FOB ".. _UTM:upper()
    end

    -- make name unique
    _fobName = string.format("%s #%i", _fobName, veaf.getUniqueIdentifier())

    -- spawn the FOB buildings
    local _outpost = {
        category = "Fortifications",
        type = "outpost",
        y = _spawnPosition.z,
        x = _spawnPosition.x,
        name = _fobName,
        canCargo = false,
        heading = mist.utils.toRadian(hdg),
        country = _country
    }
    mist.dynAddStatic(_outpost)
    local _fob = StaticObject.getByName(_outpost["name"])

    local _tower = {
        type = "house2arm",
        rate = 100,
        y = _outpost.y + TOWER_DISTANCE * math.sin(mist.utils.toRadian(_hdg)),
        x = _outpost.x + TOWER_DISTANCE * math.cos(mist.utils.toRadian(_hdg)),
        name = _fobName .. " Watchtower #002",
        category = "Fortifications",
        canCargo = false,
        heading = mist.utils.toRadian(hdg),
        country = _country
    }
    mist.dynAddStatic(_tower)

    --make it able to deploy crates and pickup troops
    table.insert(ctld.logisticUnits, _fobName)
    table.insert(ctld.builtFOBS, _fobName)

    -- add the FOB to the named points
    local _namedPoint = _spawnPosition
    _namedPoint.atc = true
    _namedPoint.runways = {}

    -- spawn a beacon
    local _beaconPoint = {
        z = _tower.y + BEACON_DISTANCE * math.sin(mist.utils.toRadian(_hdg)),
        x = _tower.x + BEACON_DISTANCE * math.cos(mist.utils.toRadian(_hdg)),
        y = _spawnPosition.y,
    }
    ctld.beaconCount = ctld.beaconCount + 1
    local _radioBeaconName = "FOB Beacon #" .. ctld.beaconCount
    local _radioBeaconDetails = ctld.createRadioBeacon(_beaconPoint, _side, _country, _radioBeaconName, nil, true)
    ctld.fobBeacons[_fobName] = { vhf = _radioBeaconDetails.vhf, uhf = _radioBeaconDetails.uhf, fm = _radioBeaconDetails.fm }
    if _radioBeaconDetails ~= nil then
        _namedPoint.tacan = string.format("ADF : %.2f KHz - %.2f MHz - %.2f MHz FM", _radioBeaconDetails.vhf / 1000, _radioBeaconDetails.uhf / 1000000, _radioBeaconDetails.fm / 1000000)
        veaf.loggers.get(veafSpawn.Id):trace("_namedPoint.tacan=%s", veaf.p(_namedPoint.tacan))
    end
    trigger.action.outTextForCoalition(_side, string.format("Finished building FOB %s! Crates and Troops can now be picked up.", _fobName), 10)

	_namedPoint.tower = "No Control"

    veaf.loggers.get(veafSpawn.Id):trace("_namedPoint=%s", veaf.p(_namedPoint))

	veafNamedPoints.addPoint(_fobName, _namedPoint)

    veaf.loggers.get(veafSpawn.Id):info("Spawned FOB %s", veaf.p(_fobName))
    return _fobName
end

--- Spawn a specific group at a specific spot
function veafSpawn.spawnGroup(spawnSpot, radius, name, czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD)

    veaf.loggers.get(veafSpawn.Id):debug("spawnGroup(name=%s, czName=%s, country=%s, alt=%s, hdg=%s, spacing=%s, groupName=%s, silent=%s, hiddenOnMFD=%s)", name, czName, country, alt, hdg, spacing, silent, groupName, hiddenOnMFD)

    local spawnedGroupName = veafSpawn.doSpawnGroup(spawnSpot, radius, name, czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD)

    return spawnedGroupName
end

local function validateSpawnPosition(spawnPosition, unit, silent)
    if not veafUnits.checkPositionForUnit(spawnPosition, unit) then
        veaf.loggers.get(veafSpawn.Id):info("Cannot find a suitable position for spawning unit " .. unit.typeName)
        if not silent then
            trigger.action.outText("Cannot find a suitable position for spawning unit " .. unit.typeName, 5)
        end
        return false
    end
    return true
end

function veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD, hasDest)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn._createDcsUnits([%s])",country or ""))

    if hasDest then
        mist.scheduleFunction(veafUnits.removePathfindingFixUnit,{groupName}, timer.getTime()+veafUnits.delayBeforePathfindingFix)
    end

    local dcsUnits = {}
    for i=1, #units do
        local unit = units[i]
        local unitType = unit.typeName
        local unitNameTemplate = "%s - %s"
        if veafSpawn.HideTypeFromGroupNames then
            unitNameTemplate = "%s"
        end
        local unitName = string.format(unitNameTemplate, groupName, unit.displayName)
        local spawnPosition = unit.spawnPoint
        local hdg = spawnPosition.hdg or math.random(0, 359)

        if validateSpawnPosition(spawnPosition, unit, false) then
            local toInsert = {
                    ["x"] = spawnPosition.x,
                    ["y"] = spawnPosition.z,
                    ["alt"] = spawnPosition.y,
                    ["type"] = unitType,
                    ["name"] = unitName,
                    ["speed"] = 0,
                    ["skill"] = "Excellent",
                    ["heading"] = hdg
            }

            veaf.loggers.get(veafSpawn.Id):trace(string.format("toInsert x=%.1f y=%.1f, alt=%.1f, type=%s, name=%s, speed=%d, heading=%d, skill=%s, country=%s", toInsert.x, toInsert.y, toInsert.alt, toInsert.type, toInsert.name, toInsert.speed, toInsert.heading, toInsert.skill, country ))
            table.insert(dcsUnits, toInsert)
        end
    end

    -- actually spawn groups
    mist.dynAdd({country = country, category = "GROUND_UNIT", name = groupName, hidden = false, units = dcsUnits, hiddenOnMFD = hiddenOnMFD})
end

--- Spawns a dynamic infantry group 
function veafSpawn.spawnInfantryGroup(spawnSpot, radius, czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnInfantryGroup(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD)

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot))
    local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Infantry Section", czName)
    local group = veafCasMission.generateInfantryGroup(groupName, defense, armor, side, size)
    local group = veafUnits.processGroup(group)
    local groupPosition = veaf.placePointOnLand(spawnSpot)
    veaf.loggers.get(veafSpawn.Id):trace(string.format("groupPosition = %s",veaf.vecToString(groupPosition)))
    local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading)

    -- shuffle the units in the group
    local units = veaf.shuffle(group.units)

    veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD)

    if not silent then
        trigger.action.outText("Spawned dynamic infantry group "..groupName, 5)
    end

    return groupName
end

--- Spawns a dynamic armored platoon
function veafSpawn.spawnArmoredPlatoon(spawnSpot, radius, czName, country, side, heading, spacing, defense, armor, size, silent, hasDest, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnArmoredPlatoon(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s",spawnSpot)
    local randSpot = mist.getRandPointInCircle(spawnSpot, radius)
    veaf.loggers.get(veafSpawn.Id):trace("randSpot=%s",randSpot)
    local spawnSpot = veaf.placePointOnLand(randSpot)
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s",spawnSpot)
    local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Armored Platoon", czName)
    veaf.loggers.get(veafSpawn.Id):trace("groupName=%s",groupName)
    local group = veafCasMission.generateArmorPlatoon(groupName, defense, armor, side, size)
    local group = veafUnits.processGroup(group)
    local groupPosition = veaf.placePointOnLand(spawnSpot)
    veaf.loggers.get(veafSpawn.Id):trace("groupPosition=%s",groupPosition)
    local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading, hasDest)

    -- shuffle the units in the group
    local units = group.units
    if not(hasDest) then
        units = veaf.shuffle(group.units)
    end

    veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD, hasDest)

    if not silent then
        trigger.action.outText("Spawned dynamic armored platoon "..groupName, 5)
    end

    return groupName
end

--- Spawns a dynamic air defense battery
function veafSpawn.spawnAirDefenseBattery(spawnSpot, radius, czName, country, side, heading, spacing, defense, silent, hasDest, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnAirDefenseBattery(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD)

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot))
    local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Air Defense Battery", czName)
    local group = veafCasMission.generateAirDefenseGroup(groupName, defense, side)
    local group = veafUnits.processGroup(group)
    local groupPosition = veaf.placePointOnLand(spawnSpot)
    veaf.loggers.get(veafSpawn.Id):trace(string.format("groupPosition = %s",veaf.vecToString(groupPosition)))
    local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading, hasDest)

    -- shuffle the units in the group
    local units = group.units
    if not(hasDest) then
        units = veaf.shuffle(group.units)
    end

    veafSpawn._createDcsUnits(country or veaf.getCountryForCoalition(side), units, groupName, hiddenOnMFD, hasDest)

    if not silent then
        trigger.action.outText("Spawned dynamic air defense battery "..groupName, 5)
    end

    return groupName
end

--- Spawns a dynamic transport company
function veafSpawn.spawnTransportCompany(spawnSpot, radius, czName, country, side, heading, spacing, defense, size, silent, hasDest, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnTransportCompany(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD)

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot))
    local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Transport Company", czName)
    local group = veafCasMission.generateTransportCompany(groupName, defense, side, size)
    local group = veafUnits.processGroup(group)
    local groupPosition = veaf.placePointOnLand(spawnSpot)
    veaf.loggers.get(veafSpawn.Id):trace(string.format("groupPosition = %s",veaf.vecToString(groupPosition)))
    local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading, hasDest)

    -- shuffle the units in the group
    local units = group.units
    if not(hasDest) then
        units = veaf.shuffle(group.units)
    end

    veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD, hasDest)

    if not silent then
        trigger.action.outText("Spawned dynamic transport company "..groupName, 5)
    end

    return groupName
end

--- Spawns a dynamic full combat group composed of multiple platoons
function veafSpawn.spawnFullCombatGroup(spawnSpot, radius, czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnFullCombatGroup(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD)

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot))
    local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Full Combat Group", czName)
    local groupPosition = veaf.placePointOnLand(spawnSpot)
    local units = veafCasMission.generateCasGroup(groupName, groupPosition, size, defense, armor, spacing, side)

    veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD)

    if not silent then
        trigger.action.outText("Spawned full combat group "..groupName, 5)
    end

    return groupName
end

--- Spawn a specific group at a specific spot
function veafSpawn.spawnConvoy(spawnSpot, name, czName, radius, country, side, heading, spacing, speed, patrol, offroad, destination, defense, size, armor, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnConvoy(spawnSpot=[%s], name=[%s], radius=[%s], country=[%s], side=[%s], speed=[%s], patrol=[%s], offroad=[%s], destination=[%s], defense=[%s], size=[%s], armor=[%s], silent=[%s], hiddenOnMFD=[%s])", czName, spawnSpot, name, radius, country, side, speed, patrol, offroad, destination, defense, size, armor, silent, hiddenOnMFD)

    if not(destination) then
        trigger.action.outText("No destination enterred !", 5)
        return false
    end

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot))

    -- check that destination exists
    local point = nil
    if destination then
        point = veafNamedPoints.getPoint(destination)
    end
    if not(point) then
        local _lat, _lon = veaf.computeLLFromString(destination)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("_lat=%s",veaf.p(_lat)))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("_lon=%s",veaf.p(_lon)))
        if _lat and _lon then
            point = coord.LLtoLO(_lat, _lon)
            veaf.loggers.get(veafSpawn.Id):trace(string.format("point=%s",veaf.p(point)))
        end
    end
    if not(point) then
        trigger.action.outText("A point named "..destination.." cannot be found, and these are not valid coordinates !", 5)
        return false
    end

    local groupUnits = {}
    groupUnits.units = {}
    local groupId = math.random(99999)
    local groupName = name
    if not groupName or groupName == "" then
        groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Convoy", czName)
    end

    -- generate the transport vehicles and air defense
    if size and size > 0 then -- this is only for reading clarity sake
        -- generate the group
        local group = veafCasMission.generateTransportCompany(groupId, defense, side, size)

        -- process the group 
        local group = veafUnits.processGroup(group)

        -- add the units to the global units list
        for _,u in pairs(group.units) do
            table.insert(groupUnits.units, u)
        end
    end

    -- generate the armored vehicles
    if armor and armor > 0 then
        -- generate the group
        local group = veafCasMission.generateArmorPlatoon(groupId, defense, armor, side, size / 2)

        -- process the group 
        local group = veafUnits.processGroup(group)

        -- add the units to the global units list
        for _,u in pairs(group.units) do
            table.insert(groupUnits.units, u)
        end
    end

    if groupUnits.units then
        -- place its units
        local groupUnits, cells = veafUnits.placeGroup(groupUnits, veaf.placePointOnLand(spawnSpot), spacing, heading, true)
        veafUnits.traceGroup(groupUnits, cells)

        -- shuffle the units in the convoy
        --disabled the shuffle to not have interractions with the line spawn put in place for faster departure times, which shuffles units anyways
        --units = veaf.shuffle(units)

        veafSpawn._createDcsUnits(country, groupUnits.units, groupName, hiddenOnMFD, true)

        local route = veaf.generateVehiclesRoute(spawnSpot, destination, not offroad, speed, patrol, groupName)
        veafSpawn.spawnedConvoys[groupName] = {route=route, name=groupName}

        --  make the group go to destination
        veaf.loggers.get(veafSpawn.Id):trace("make the group go to destination : ".. groupName)
        mist.goRoute(groupName, route)

        if not silent then
            trigger.action.outText("Spawned convoy "..groupName, 5)
        end
    end

    return groupName
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Unit spawn command
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Spawn a specific unit at a specific spot
-- @param position spawnPosition
-- @param string name
-- @param string country
-- @param int speed
-- @param int alt
-- @param int speed
-- @param int hdg (0..359)
-- @param string unitName (callsign)
-- @param string role (ex: jtac)
-- @param boolean static (is the unit force to spawn as a static unit)
-- @param integer code (starts at 1111, laser code if jtac)
-- @param string freq (frequency if JTAC in MHz with . separator)
-- @param boolean silent (mutes messages to players except errors)
-- @param boolean hiddenOnMFD
function veafSpawn.spawnUnit(spawnPosition, radius, name, czName, country, alt, hdg, unitName, role, static, code, freq, mod, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnUnit(name = %s, czName=%s, country=%s, alt=%d, hdg=%d, unitName=%s, role=%s, static=%s, code=%s, freq=%s, mod=%s, silent=%s, hiddenOnMFD=%s)", name, czName, country, alt, hdg, unitName, role, static, code, freq, mod, silent, hiddenOnMFD)

    veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1

    -- find the desired unit in the groups database
    local unit = veafUnits.findUnit(name)

    if not(unit) then
        veaf.loggers.get(veafSpawn.Id):info("cannot find unit "..name)
        trigger.action.outText("cannot find unit "..name, 5)
        return
    end

    -- cannot spawn planes or helos yet [TODO], however spawning them as a static is fine
    if unit.air and not static then
        veaf.loggers.get(veafSpawn.Id):info("Air units cannot be spawned at the moment (work in progress)")
        trigger.action.outText("Air units cannot be spawned at the moment (work in progress)", 5)
        return
    end

    local units = {}
    local groupName = nil

    veaf.loggers.get(veafSpawn.Id):trace("spawnUnit unit = " .. unit.displayName .. ", dcsUnit = " .. tostring(unit.typeName))

    if role == "jtac" then
        local name = "JTAC " .. tostring(code):sub(1,1) .. " " .. tostring(code):sub(2,2) .. " " .. tostring(code):sub(3,3) .. " " .. tostring(code):sub(4,4)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("name=%s", tostring(name)))
        groupName = name
        unitName = name
    elseif role == "tacan" then
        local name = "TACAN " .. tostring(freq)..tostring(mod)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("name=%s", tostring(name)))
        groupName = name
        unitName = name
    else
        groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), name, czName)
        if not unitName then
            unitName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), unit.displayName, czName)
        end
    end

    veaf.loggers.get(veafSpawn.Id):trace("groupName="..groupName)
    veaf.loggers.get(veafSpawn.Id):trace("unitName="..unitName)

    local spawnSpot = nil
    local nbTries = 25
    repeat
        spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnPosition, radius))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnUnit: spawnSpot  x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z))
        if alt > 0 then
            spawnSpot.y = alt
        end
        if not veafUnits.checkPositionForUnit(spawnSpot, unit) then
            veaf.loggers.get(veafSpawn.Id):debug("finding another spawnSpot for unit %s, remaining tries #%s", unit.displayName, nbTries)
            spawnSpot = nil
            nbTries = nbTries - 1
        end
    until spawnSpot or nbTries <= 0

    if not spawnSpot then
        veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning unit "..unit.displayName)
        trigger.action.outText("cannot find a suitable position for spawning unit "..unit.displayName, 5)
        return
    else
        local toInsert = {}
        local effectPreset = nil
        local effectTransparency = nil
        local shapeName = nil

        if unit.static or static then

            if unit.category then
                if unit.category == "Heliport" then
                    unit.category = "Heliports"
                end
                -- if unit.category == "Effect" then
                --     unit.category = "Effects"
                --     effectPreset = 2
                --     effectTransparency = 1
                --     shapeName = "medium smoke and fire"
                -- end
            end

            groupName = unitName --this name here will be used for reference by DCS, since we return groupName for other scripts to do their thing, this must be the unitName

            toInsert = {
                ["x"] = spawnSpot.x,
                ["y"] = spawnSpot.z,
                ["alt"] = spawnSpot.y,
                ["type"] = unit.typeName,
                ["name"] = groupName,
                ["category"] = unit.category,
                ["heading"] = mist.utils.toRadian(hdg),
                -- ["effectTransparency"] = effectTransparency,
                -- ["effectPreset"] = effectPreset,
                -- ["shapeName"] = shapeName,
            }
        else
            toInsert = {
                ["x"] = spawnSpot.x,
                ["y"] = spawnSpot.z,
                ["alt"] = spawnSpot.y,
                ["type"] = unit.typeName,
                ["name"] = unitName,
                ["speed"] = 0,
                ["skill"] = "Random",
                ["heading"] = mist.utils.toRadian(hdg),
            }
        end

        table.insert(units, toInsert)
    end

    veaf.loggers.get(veafSpawn.Id):trace(string.format("unitData = %s", veaf.p(units)))

    -- actually spawn the unit
    if unit.static or static then --if the unit was forced to spawn as a static it could still be an air or a naval unit so this check goes first
        veaf.loggers.get(veafSpawn.Id):trace("Spawning STATIC")
        mist.dynAddStatic({country = country, groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD})
        --groupName = nil --statics do not have a group name, you must set groupName to nil to avoid other scripts interacting
    elseif unit.air then
        veaf.loggers.get(veafSpawn.Id):trace("Spawning AIRPLANE")
        mist.dynAdd({country = country, category = "PLANE", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD})
    elseif unit.naval then
        veaf.loggers.get(veafSpawn.Id):trace("Spawning SHIP")
        mist.dynAdd({country = country, category = "SHIP", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD})
    else
        veaf.loggers.get(veafSpawn.Id):trace("Spawning GROUND_UNIT")
        mist.dynAdd({country = country, category = "GROUND_UNIT", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD})
    end

    if role == "jtac" and not static then
        -- JTAC needs to be invisible and immortal
        local _setImmortal = {
            id = 'SetImmortal',
            params = {
                value = true
            }
        }
        -- invisible to AI, Shagrat
        local _setInvisible = {
            id = 'SetInvisible',
            params = {
                value = true
            }
        }

        local spawnedGroup = Group.getByName(groupName)
        local controller = spawnedGroup:getController()
        Controller.setCommand(controller, _setImmortal)
        Controller.setCommand(controller, _setInvisible)

        -- start lasing 
        if ctld then
            ctld.cleanupJTAC(groupName)
            local radioData = {freq=freq, mod=mod, name=groupName}
            veafSpawn.JTACAutoLase(groupName, code, radioData)
        end

    elseif role == "tacan" and not static then
        veaf.loggers.get(veafSpawn.Id):trace(string.format("name=%s", tostring(name)))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("freq=%s", tostring(freq)))
        local mod = string.upper(mod) or "X"
        veaf.loggers.get(veafSpawn.Id):trace(string.format("mod=%s", tostring(mod)))
        local txFreq = (1025 + freq - 1) * 1000000
        local rxFreq = (962 + freq - 1) * 1000000
        if (freq < 64 and mod == "Y") or (freq >= 64 and mod == "X") then
            rxFreq = (1088 + freq - 1) * 1000000
        end
        veaf.loggers.get(veafSpawn.Id):trace(string.format("txFreq=%s", tostring(txFreq)))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("rxFreq=%s", tostring(rxFreq)))

        local command = {
            id = 'ActivateBeacon',
            params = {
                type = 4,
                system = 18,
                callsign = code or "TCN",
                frequency = rxFreq,
                AA = false,
                channel = freq,
                bearing = true,
                modeChannel = mod,
            }
        }

        veaf.loggers.get(veafSpawn.Id):trace(string.format("setting %s", veaf.p(command)))
        local spawnedGroup = Group.getByName(groupName)
        local controller = spawnedGroup:getController()
        controller:setCommand(command)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("done setting command"))
    end

    -- message the unit spawning
    veaf.loggers.get(veafSpawn.Id):trace(string.format("message the unit spawning"))
    if (role == "jtac") or not silent then
        local message = "A " .. unit.displayName .. " ("..country..") has been spawned"
        if role == "jtac" and not static then
            message = "JTAC spawned, lasing on "..code..", available on "..freq.." "..mod
        end
        veaf.loggers.get(veafSpawn.Id):trace(message)
        trigger.action.outText(message, 15)
    end

    return groupName
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Cargo spawn command
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Spawn a specific cargo at a specific spot
function veafSpawn.spawnCargo(spawnSpot, radius, cargoType, country, weightBias, cargoSmoke, unitName, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnCargo(cargoType = " .. cargoType ..")")

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: spawnSpot  x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z))

    return veafSpawn.doSpawnCargo(spawnSpot, radius, cargoType, country, weightBias, unitName, cargoSmoke, silent, hiddenOnMFD)
end

--- Spawn a logistic unit for CTLD at a specific spot
function veafSpawn.spawnLogistic(spawnSpot, radius, country, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("spawnLogistic()")

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnLogistic: spawnSpot  x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z))

    local unitName = veafSpawn.doSpawnStatic(spawnSpot, radius, veafSpawn.LogisticUnitCategory, veafSpawn.LogisticUnitType, country, nil, false, true, hiddenOnMFD)

    if unitName then
        veaf.loggers.get(veafSpawn.Id):debug(string.format("spawnLogistic: inserting %s into CTLD logistics list", unitName))
        if ctld then
            table.insert(ctld.logisticUnits, unitName)
        end

        -- message the unit spawning
        if not silent then
            local message = "Logistic unit " .. unitName .. " has been spawned and was added to CTLD."
            trigger.action.outText(message, 15)
        end
        return unitName
    else
        local message = "Logistic unit could not be spawned"
        trigger.action.outText(message, 15)
        return
    end
end

--- Spawn a specific cargo at a specific spot
function veafSpawn.doSpawnCargo(spawnSpot, radius, cargoType, country, weightBias, unitName, cargoSmoke, silent, hiddenOnMFD)
    local weightBias = weightBias or 2
    local radius = radius or 0
    veaf.loggers.get(veafSpawn.Id):debug("spawnCargo(cargoType = " .. cargoType ..")")

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: spawnSpot  x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z))

    local units = {}

    local spawnPosition = veaf.findPointInZone(spawnSpot)

    -- check spawned position validity
    if spawnPosition == nil then
        veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning cargo "..cargoType)
        trigger.action.outText("cannot find a suitable position for spawning cargo "..cargoType, 5)
        return
    end

    veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: spawnPosition  x=%.1f y=%.1f", spawnPosition.x, spawnPosition.y))

    -- compute cargo weight
    local cargoWeight = 250
    local unit = veafUnits.findDcsUnit(cargoType)
    if not unit then
        cargoType = cargoType.. "_cargo"
        unit = veafUnits.findDcsUnit(cargoType)
    end
    if unit then
        if unit.type then
            cargoType = unit.type
        else
            veaf.loggers.get(veafSpawn.Id):info("could not find cargo type named ".. veaf.p(cargoType))
            trigger.action.outText("could not find cargo type named ".. veaf.p(cargoType), 15)
            return
        end

        veaf.loggers.get(veafSpawn.Id):debug(string.format("weightBias=%s", veaf.p(weightBias)))
        if unit.desc and unit.desc.minMass and unit.desc.maxMass then
            local weightScaleRange = veafSpawn.cargoWeightBiasRange + 1
            local massDelta = unit.desc.maxMass - unit.desc.minMass
            if massDelta < 0 then --never can be too careful around DCS
                local temp = unit.desc.maxMass
                unit.desc.maxMass = unit.desc.minMass
                unit.desc.minMass = temp
                massDelta = math.abs(massDelta)
            end
            local minMass = unit.desc.minMass + weightBias * massDelta / weightScaleRange
            local maxMass =  unit.desc.minMass + (weightBias+1) * massDelta / weightScaleRange
            veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo minMass=%s, cargo maxMass=%s", veaf.p(minMass), veaf.p(maxMass)))
            cargoWeight = math.random(minMass,maxMass)
        elseif unit.defaultMass then
            local BiasOffset = -math.floor(veafSpawn.cargoWeightBiasRange / 2)
            local weightBiasCentered = weightBias + BiasOffset
            local cargoWeightBiasScaleMin = BiasOffset
            local cargoWeightBiasScaleMax = veafSpawn.cargoWeightBiasRange + BiasOffset
            local weightBiasMax = weightBiasCentered + 1
            local weightBiasMin = weightBiasCentered

            cargoWeight = unit.defaultMass
            veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo defaultMass=%s", veaf.p(cargoWeight)))
            local minMass = cargoWeight + weightBiasMin * cargoWeight / (2*cargoWeightBiasScaleMax)
            local maxMass = cargoWeight + weightBiasMax * cargoWeight / (2*cargoWeightBiasScaleMax)
            veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo minMass=%s, cargo maxMass=%s", veaf.p(minMass), veaf.p(maxMass)))
            cargoWeight = math.random(minMass,maxMass)
        end
        if cargoWeight then
            veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo mass=%s", veaf.p(cargoWeight)))

            if not(unitName) then
                veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1
                unitName = unit.name .. " #" .. veafSpawn.spawnedUnitsCounter
            end

            -- create the cargo
            local cargoTable = {
                type = cargoType,
                country = country,
                category = 'Cargos',
                name = unitName,
                x = spawnPosition.x,
                y = spawnPosition.y,
                canCargo = true,
                mass = cargoWeight,
                hiddenOnMFD = hiddenOnMFD,
            }

            mist.dynAddStatic(cargoTable)

            -- smoke the cargo if needed
            if cargoSmoke then
                local smokePosition={x=spawnPosition.x + mist.random(10,20), y=0, z=spawnPosition.y + mist.random(10,20)}
                local height = veaf.getLandHeight(smokePosition)
                smokePosition.y = height
                veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: smokePosition  x=%.1f y=%.1f z=%.1f", smokePosition.x, smokePosition.y, smokePosition.z))
                veafSpawn.spawnSmoke(smokePosition, trigger.smokeColor.Green)
                for i = 1, 10 do
                    veaf.loggers.get(veafSpawn.Id):trace("Signal flare 1 at " .. timer.getTime() + i*7)
                    mist.scheduleFunction(veafSpawn.spawnSignalFlare, {smokePosition, nil, nil, trigger.flareColor.Red}, timer.getTime() + i*3)
                end
            end

            -- message the unit spawning
            local message = "Cargo " .. unitName .. " weighting " .. cargoWeight .. " kg has been spawned"
            if cargoSmoke then
                message = message .. ". It's marked with green smoke and red flares"
            end
            if not(silent) then trigger.action.outText(message, 15) end
        end
    else
        veaf.loggers.get(veafSpawn.Id):info("could not find cargo type named ".. veaf.p(cargoType))
        trigger.action.outText("could not find cargo type named ".. veaf.p(cargoType), 15)
        return
    end
    return unitName
end


--- Spawn a specific static at a specific spot
function veafSpawn.doSpawnStatic(spawnSpot, radius, staticCategory, staticType, country, unitName, smoke, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("doSpawnStatic(staticCategory = " .. staticCategory ..")")
    veaf.loggers.get(veafSpawn.Id):debug("doSpawnStatic(staticType = " .. staticType ..")")

    local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("doSpawnStatic: spawnSpot  x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z))

    local units = {}

    local spawnPosition = veaf.findPointInZone(spawnSpot, 50, false)

    -- check spawned position validity
    if spawnPosition == nil then
        veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning static "..staticType)
        if not(silent) then trigger.action.outText("cannot find a suitable position for spawning static "..staticType, 5) end
        return
    end

    veaf.loggers.get(veafSpawn.Id):trace(string.format("doSpawnStatic: spawnPosition  x=%.1f y=%.1f", spawnPosition.x, spawnPosition.y))

    local unit = veafUnits.findDcsUnit(staticType)
    if unit then
        if not(unitName) then
            veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1
            unitName = unit.name .. " #" .. veafSpawn.spawnedUnitsCounter
        end

        -- create the static
        local staticTable = {
            category = staticCategory,
            type = staticType,
            country = country,
            name = unitName,
            x = spawnPosition.x,
            y = spawnPosition.y,
            hiddenOnMFD = hiddenOnMFD,
        }

        mist.dynAddStatic(staticTable)

        -- smoke if needed
        if smoke then
            local smokePosition={x=spawnPosition.x + mist.random(10,20), y=0, z=spawnPosition.y + mist.random(10,20)}
            local height = veaf.getLandHeight(smokePosition)
            smokePosition.y = height
            veaf.loggers.get(veafSpawn.Id):trace(string.format("doSpawnStatic: smokePosition  x=%.1f y=%.1f z=%.1f", smokePosition.x, smokePosition.y, smokePosition.z))
            veafSpawn.spawnSmoke(smokePosition, trigger.smokeColor.Green)
            for i = 1, 10 do
                veaf.loggers.get(veafSpawn.Id):trace("Signal flare 1 at " .. timer.getTime() + i*7)
                mist.scheduleFunction(veafSpawn.spawnSignalFlare, {smokePosition, nil, nil, trigger.flareColor.Red}, timer.getTime() + i*3)
            end
        end

        -- message the unit spawning
        local message = "Static " .. unitName .. " has been spawned"
        if smoke then
            message = message .. ". It's marked with green smoke and red flares"
        end
        if not(silent) then trigger.action.outText(message, 5) end
    end
    return unitName
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Smoke and Flare commands
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- trigger an explosion at the marker area
function veafSpawn.spawnBomb(spawnSpot, radius, shells, power, altitude, altitudedelta, password)
    veaf.loggers.get(veafSpawn.Id):debug("spawnBomb(power=" .. power ..")")

    local shellTime = 0
    local shellDelay = 0
    for shell=1,shells do
        local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
        veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", spawnSpot)
        veaf.loggers.get(veafSpawn.Id):trace("altitude=%s", altitude)
        if altitude and altitude > 0 then
            spawnSpot.y = altitude + altitudedelta * ((math.random(100)-50)/100)
            shellDelay = veafSpawn.FlakingInterval
        else
            shellDelay = veafSpawn.ShellingInterval
        end
        veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", spawnSpot)

        local shellDelay = shellDelay * (math.random(100) + 30)/100
        local shellPower = power * (math.random(100) + 30)/100
        -- check security
        if not veafSecurity.checkPassword_L0(password) then
            if shellPower > 1000 then shellPower = 1000 end
        end
        veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellTime=%d, shellDelay=%d, power=%d", shell, shellTime, shellDelay, shellPower))
        mist.scheduleFunction(trigger.action.explosion, {spawnSpot, power}, timer.getTime() + shellTime)
        shellTime = shellTime + shellDelay
    end
end

--- add a smoke marker over the marker area
function veafSpawn.spawnSmoke(spawnSpot, color, radius, shells)
    veaf.loggers.get(veafSpawn.Id):debug("spawnSmoke(color=%s",veaf.p(color))
    local radius = radius or 50
    local shells = shells or 1
    veaf.loggers.get(veafSpawn.Id):trace("radius=%s", veaf.p(radius))
    veaf.loggers.get(veafSpawn.Id):trace("shells=%s", veaf.p(shells))

    local shellTime = 0
    for shell=1,shells do
        local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnSpot=%s", veaf.vecToString(spawnSpot)))

        local shellDelay = veafSpawn.ShellingInterval * (math.random(100) + 30)/100
        veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellTime=%d, shellDelay=%d", shell, shellTime, shellDelay))
        if shells > 1 then
            -- add a small explosion under the smoke to simulate smoke shells
            mist.scheduleFunction(trigger.action.explosion, {spawnSpot, 1}, timer.getTime() + shellTime-1)
        end
        mist.scheduleFunction(trigger.action.smoke, {spawnSpot, color}, timer.getTime() + shellTime)
        shellTime = shellTime + shellDelay
    end
end

--- add a signal flare over the marker area
function veafSpawn.spawnSignalFlare(spawnSpot, radius, shells, color)
    veaf.loggers.get(veafSpawn.Id):debug("spawnSignalFlare(color = " .. color ..")")

    local shellTime = 0
    for shell=1,shells do
        local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnSpot=%s", veaf.vecToString(spawnSpot)))

        local shellDelay = veafSpawn.ShellingInterval * (math.random(100) + 30)/100
        local azimuth = math.random(359)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellTime=%d, shellDelay=%d", shell, shellTime, shellDelay))
        mist.scheduleFunction(trigger.action.signalFlare, {spawnSpot, color, azimuth}, timer.getTime() + shellTime)
        shellTime = shellTime + shellDelay
    end
end

--- add an illumination flare over the target area
function veafSpawn.spawnIlluminationFlare(spawnSpot, radius, steps, power, height, heading, distance, speed)
    local radius = radius or 50
    local steps = steps or 1
    local power = power or 10
    local height = height or 500

    veaf.loggers.get(veafSpawn.Id):debug("spawnIlluminationFlare()")
    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", veaf.p(spawnSpot))
    veaf.loggers.get(veafSpawn.Id):trace("radius=%s", veaf.p(radius))
    veaf.loggers.get(veafSpawn.Id):trace("steps=%s", veaf.p(steps))
    veaf.loggers.get(veafSpawn.Id):trace("power=%s", veaf.p(power))
    veaf.loggers.get(veafSpawn.Id):trace("height=%s", veaf.p(height))
    veaf.loggers.get(veafSpawn.Id):trace("heading=%s", veaf.p(heading))
    veaf.loggers.get(veafSpawn.Id):trace("distance=%s", veaf.p(distance))


    local cosHeading
    local sinHeading
    local stepDistance
    if heading then
        if distance then
            distance = distance * 1852 -- meters
            stepDistance = distance / (steps - 1)
        elseif speed then
            speed = speed / 1.94384 -- m/s
            stepDistance = speed * veafSpawn.IlluminationShellingInterval
        end
        local headingRad = mist.utils.toRadian(heading)
        cosHeading = math.cos(headingRad)
        sinHeading = math.sin(headingRad)
    end

    local stepTime = 0
    for step=1, steps do
        local stepDelay = veafSpawn.IlluminationShellingInterval * (math.random(100, 130)-15)/100
        local newSpawnSpot = mist.utils.deepCopy(spawnSpot)
        if stepDistance then
            newSpawnSpot.x = spawnSpot.x + stepDistance * (step - 1) * cosHeading
            newSpawnSpot.z = spawnSpot.z + stepDistance * (step - 1) * sinHeading
        end
        local shellsPerStep = math.random(5, 10)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("step #%d : stepTime=%d, shellDelay=%d", step, stepTime, stepDelay))
        for shell=1, shellsPerStep do
            local shellDelay = shell/4 + (math.random(100, 150)-25)/100
            local shellHeight = height * (math.random(100, 130)-15)/100
            local shellPower = power * (math.random(100, 130)-15)/100
            local newSpawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(newSpawnSpot, radius))
            newSpawnSpot.y = veaf.getLandHeight(newSpawnSpot) + shellHeight
            veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellHeight=%d, shellPower=%d", shell, shellHeight, shellPower))
            local time = timer.getTime() + stepTime + shellDelay
            -- add a small explosion under the flare to simulate flare shells
            mist.scheduleFunction(trigger.action.explosion, {newSpawnSpot, 0.1}, time)
            mist.scheduleFunction(trigger.action.illuminationBomb, {newSpawnSpot, shellPower}, time)
        end
        stepTime = stepTime + stepDelay
    end
end

--- FLAK-related constants
veafSpawn.NB_OF_FLAKS_AT_DENSITY_1 = 30
veafSpawn.DEFAULT_FLAK_CLOUD_SIZE = 30
veafSpawn.DEFAULT_FLAK_POWER = 1
veafSpawn.DEFAULT_FLAK_REPEAT_DELAY = 0.2
veafSpawn.DEFAULT_FLAK_FIRE_DELAY = 0.1

function veafSpawn.destroyObjectWithFlak(object, power, density)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.destroyObjectWithFlak(%s, %s, %s)", veaf.p(power), veaf.p(power), veaf.p(density)))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("object=%s", veaf.p(object)))
    local _power = power or veafSpawn.DEFAULT_FLAK_POWER
    local _density = density or 1

    if object and object:isExist() then
        local point = object:getPoint()
        local positionForFlak = mist.vec.add(point, mist.vec.scalarMult(object:getVelocity(), veafSpawn.DEFAULT_FLAK_FIRE_DELAY))
        local nbFlaks = veafSpawn.NB_OF_FLAKS_AT_DENSITY_1 * _density
        veaf.loggers.get(veafSpawn.Id):trace(string.format("firing %d flak shells", nbFlaks))
        for i = 1, nbFlaks do
            local flakPoint = {
                x = point.x + (veafSpawn.DEFAULT_FLAK_CLOUD_SIZE * math.random(-100,100) / 100),
                y = point.y + (veafSpawn.DEFAULT_FLAK_CLOUD_SIZE * math.random(-100,100) / 100),
                z = point.z + (veafSpawn.DEFAULT_FLAK_CLOUD_SIZE * math.random(-100,100) / 100)
            }
            --veaf.loggers.get(veafSpawn.Id):trace(string.format("flakPoint=%s", veaf.p(flakPoint)))
            trigger.action.explosion(flakPoint, _power)
        end

        -- reschedule to check if the object is destroyed
        veaf.loggers.get(veafSpawn.Id):trace(string.format("reschedule to check if the object is destroyed"))
        mist.scheduleFunction(veafSpawn.destroyObjectWithFlak, {object, power, power, density}, timer.getTime() + veafSpawn.DEFAULT_FLAK_REPEAT_DELAY)
    end
end

--- destroy unit(s)
function veafSpawn.destroy(spawnSpot, radius, unitName)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("destroy(radius=%s, unitName=%s)", tostring(radius), tostring(unitName)))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnSpot=%s", veaf.p(spawnSpot)))
    if unitName then
        -- destroy a specific unit
        local c = Unit.getByName(unitName)
        if c then
            veaf.loggers.get(veafSpawn.Id):trace("destroy a specific unit")
            Unit.destroy(c)
        end

        -- or a specific static
        c = StaticObject.getByName(unitName)
        if c then
            veaf.loggers.get(veafSpawn.Id):trace("destroy a specific static")
            StaticObject.destroy(c)
        end

        -- or a specific group
        c = Group.getByName(unitName)
        if c then
            veaf.loggers.get(veafSpawn.Id):trace("destroy a specific group")
            Group.destroy(c)
        end
    else
        -- radius based destruction
        veaf.loggers.get(veafSpawn.Id):trace("radius based destruction")
        local units = veaf.findUnitsInCircle(spawnSpot, radius or 150, true)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("units=%s", veaf.p(units)))
        if units then
            for name, _ in pairs(units) do
                -- try and find a  unit
                local unit = Unit.getByName(name)
                if unit then
                    Unit.destroy(unit)
                else
                    unit = StaticObject.getByName(name)
                    if unit then
                        StaticObject.destroy(unit)
                    end
                end
            end
        end
    end
end

--- teleport group
function veafSpawn.teleport(spawnSpot, name, silent)
    veaf.loggers.get(veafSpawn.Id):debug("teleport(name = " .. name ..")")
    local vars = { groupName = name, point = spawnSpot, action = "teleport" }
    local grp = mist.teleportToPoint(vars)
    if not silent then
        if grp then
            trigger.action.outText("Teleported group "..name, 5)
        else
            trigger.action.outText("Cannot teleport group : "..name, 5)
        end
    end
end

function veafSpawn._findClosestConvoy(unitName)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn._findClosestConvoy(%s)",unitName))
    local closestConvoyName = nil
    local minDistance = 99999999
    local unit = veafRadio.getHumanUnitOrWingman(unitName)
    if unit then
        for name, _ in pairs(veafSpawn.spawnedConvoys) do
            local averageGroupPosition = veaf.getAveragePosition(name)
            if not averageGroupPosition then
                veaf.loggers.get(veafSpawn.Id):error("cannot get average position of %s",veaf.p(unitName))
                return nil
            end
            local distanceFromPlayer = ((averageGroupPosition.x - unit:getPosition().p.x)^2 + (averageGroupPosition.z - unit:getPosition().p.z)^2)^0.5
            veaf.loggers.get(veafSpawn.Id):trace(string.format("distanceFromPlayer = %d",distanceFromPlayer))
            if distanceFromPlayer < minDistance then
                minDistance = distanceFromPlayer
                closestConvoyName = name
                veaf.loggers.get(veafSpawn.Id):trace(string.format("convoy %s is closest",closestConvoyName))
            end
        end
    end
    return closestConvoyName
end

function veafSpawn._commandConvoy(convoyName, stop)
    local group = Group.getByName(convoyName)
    if group then
        if stop then
            local stopped = veafSpawn.spawnedConvoys[convoyName].stopped
            if stopped then
                -- already stopped !
                return false
            else
                local task ={
                    id = 'Hold',
                    params = { }
                    }
                    group:getController():pushTask(task)
                veafSpawn.spawnedConvoys[convoyName].stopped = true
            end
        else
            local stopped = veafSpawn.spawnedConvoys[convoyName].stopped
            if stopped then
                mist.goRoute(convoyName, veafSpawn.spawnedConvoys[convoyName].route)
                veafSpawn.spawnedConvoys[convoyName].stopped = false
            else
                -- not stopped !
                return false
            end
        end
    end
end

function veafSpawn.stopClosestConvoy(unitName)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.stopClosestConvoy(unitName=%s)",unitName))
    local convoyName = veafSpawn._findClosestConvoy(unitName)
    if convoyName then
        return veafSpawn._commandConvoy(convoyName, true)
    end
end

function veafSpawn.moveClosestConvoy(unitName)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.moveClosestConvoy(unitName=%s)",unitName))
    local convoyName = veafSpawn._findClosestConvoy(unitName)
    if convoyName then
        return veafSpawn._commandConvoy(convoyName, false)
    end
end

function veafSpawn._markClosestConvoyWithSmoke(unitName, markRoute)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.markClosestConvoyWithSmoke(unitName=%s)",unitName))
    local closestConvoyName = veafSpawn._findClosestConvoy(unitName)
    if closestConvoyName then
        if markRoute then
            local route = veafSpawn.spawnedConvoys[closestConvoyName].route
            local startPoint = veaf.placePointOnLand({x = route[1].x, y = 0, z = route[1].y})
            local endPoint = veaf.placePointOnLand({x = route[2].x, y = 0, z = route[2].y})
            trigger.action.smoke(startPoint, trigger.smokeColor.Green)
            trigger.action.smoke(endPoint, trigger.smokeColor.Red)
            veaf.outTextForUnit(unitName, closestConvoyName .. " is going from green to red smoke", 10)
        else
            local averageGroupPosition = veaf.getAveragePosition(closestConvoyName)
            trigger.action.smoke(averageGroupPosition, trigger.smokeColor.White)
            veaf.outTextForUnit(unitName, closestConvoyName .. " marked with white smoke", 10)
        end
    else
        veaf.outTextForUnit(unitName, "No convoy found", 10)
    end
end

function veafSpawn.markClosestConvoyWithSmoke(unitName)
    return veafSpawn._markClosestConvoyWithSmoke(unitName, false)
end

function veafSpawn.markClosestConvoyRouteWithSmoke(unitName)
    return veafSpawn._markClosestConvoyWithSmoke(unitName, true)
end

function veafSpawn.infoOnAllConvoys(unitName)
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.infoOnAllConvoys(unitName=%s)",unitName))
    local text = ""
    for name, _ in pairs(veafSpawn.spawnedConvoys) do
        local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(name)
        if nbVehicles > 0 then
            local averageGroupPosition = veaf.getAveragePosition(name)
            local lat, lon = coord.LOtoLL(averageGroupPosition)
            local llString = mist.tostringLL(lat, lon, 0, true)
            text = text .. " - " .. name .. ", " .. nbVehicles .. " vehicles : " .. llString
            if veafSpawn.spawnedConvoys[name].stopped then
                text = text .. ", stopped"
            end
        else
            text = text .. " - " .. name .. "has been destroyed"
            -- convoy has been dispatched, remove it from the convoys list
            veafSpawn.spawnedConvoys[name] = nil
        end
    end
    if text == "" then
        veaf.outTextForUnit(unitName, "No convoy found", 10)
    else
        veaf.outTextForUnit(unitName, text, 30)
    end
end

function veafSpawn.cleanupAllConvoys()
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.cleanupAllConvoys()")
    local foundOne = false
    for name, _ in pairs(veafSpawn.spawnedConvoys) do
        foundOne = true
        local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(name)
        if nbVehicles > 0 then
            local group = Group.getByName(name)
            if group then
                Group.destroy(group)
            end
        end
        -- convoy has been dispatched, remove it from the convoys list
        veafSpawn.spawnedConvoys[name] = nil
    end
    if foundOne then
        trigger.action.outText("All convoys cleaned up", 10)
    else
        trigger.action.outText("No convoy found", 10)
    end
end

function veafSpawn.JTACAutoLase(groupName, laserCode, radioData)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.JTACAutoLase()")
    veaf.loggers.get(veafSpawn.Id):trace(string.format("groupName=%s",tostring(groupName)))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("laserCode=%s",tostring(laserCode)))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("radioData=%s\n",veaf.p(radioData)))
    local _radio = radioData or {}
    veaf.loggers.get(veafSpawn.Id):trace(string.format("_radio=%s\n",veaf.p(_radio)))
    veaf.loggers.get(veafSpawn.Id):trace(string.format("calling CTLD"))
    ctld.JTACAutoLase(groupName, laserCode, false, "all", nil, _radio)
    veaf.loggers.get(veafSpawn.Id):trace(string.format("CTLD called"))
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- air units templates
-------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafAirUnitTemplate object
-------------------------------------------------------------------------------------------------------------------------------------------------------------
VeafAirUnitTemplate = {}

function VeafAirUnitTemplate:new(objectToCopy)
    local objectToCreate = objectToCopy or {} -- create object if user does not provide one
    setmetatable(objectToCreate, self)
    self.__index = self

    -- init the new object

    -- name
    objectToCreate.name = nil
    --  coalition (0 = neutral, 1 = red, 2 = blue)
    objectToCreate.coalition = nil
    -- route, only for veaf commands (groups already have theirs)
    objectToCreate.route = nil
    objectToCreate.humanName = nil
    objectToCreate.groupData = nil

    return objectToCreate
end

---
--- setters and getters
---

function VeafAirUnitTemplate:setName(value)
    self.name = value
    return self
end

function VeafAirUnitTemplate:getName()
    return self.name
end

function VeafAirUnitTemplate:setCoalition(value)
    self.coalition = value
    return self
end

function VeafAirUnitTemplate:getCoalition()
    return self.coalition
end

function VeafAirUnitTemplate:setGroupData(value)
    self.groupData = value
    return self
end

function VeafAirUnitTemplate:getGroupData()
    return self.groupData
end

function veafSpawn.initializeAirUnitTemplates()

    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.initializeAirUnitTemplates()")

    -- find groups with the air units template prefix
    veaf.loggers.get(veafSpawn.Id):debug("find groups with the air units template prefix")
    local _prefix = veafSpawn.AirUnitTemplatesPrefix:upper()
    veaf.loggers.get(veafSpawn.Id):trace("_prefix=%s",_prefix)
    local _templateGroups = {}
    local _groups = veaf.getGroupsOfCoalition()
    for _, group in pairs(_groups) do
        local _name = group:getName():upper()
        --veaf.loggers.get(veafSpawn.Id):trace("_name=%s",_name)
        if string.sub(_name,1,string.len(_prefix)) == _prefix then
            table.insert(_templateGroups, group)
        end
    end

    veaf.loggers.get(veafSpawn.Id):trace("_templateGroups=%s", _templateGroups)
    for _, group in pairs(_templateGroups) do
        local _groupName = group:getName()
        veaf.loggers.get(veafSpawn.Id):trace("_groupName=%s", _groupName)
        local _template = VeafAirUnitTemplate:new():setName(_groupName)
        veafSpawn.airUnitTemplates[_groupName:upper()] = _template
    end

    -- find groups within the veafSpawn.SpawnablePlanes table
    -- DOES NOT WORK YET
    if veafSpawn.SpawnablePlanes then
        veaf.loggers.get(veafSpawn.Id):debug("find groups within the veafSpawn.SpawnablePlanes table")
        for _, groupData in pairs(veafSpawn.SpawnablePlanes) do
            local _groupName = groupData.name
            veaf.loggers.get(veafSpawn.Id):trace("_groupName=%s", _groupName)
            groupData.country="russia"
            groupData.countryId=0
            groupData.category="plane"
            groupData.coalition="red"
            groupData.uncontrolled=false
            groupData.hidden=false
            local _template = VeafAirUnitTemplate:new():setName(_groupName):setGroupData(groupData)
            veafSpawn.airUnitTemplates[_groupName:upper()] = _template
        end
    end

end

function veafSpawn.listAllCAP(unitName)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.listAllCAP(unitName=%s)",unitName)
    local sorted = {}
    for name, template in pairs(veafSpawn.airUnitTemplates) do
        local _name = template:getName():sub(veafSpawn.AirUnitTemplatesPrefix:len()+1)
        table.insert(sorted, _name)
    end
    table.sort(sorted)
    local text = ""
    for _, name in pairs(sorted) do
        text = text .. name .. "\n"
    end
    if text == "" then
        veaf.outTextForUnit(unitName, "No CAP available for spawn", 10)
    else
        veaf.outTextForUnit(unitName, text, 30)
    end
end

function veafSpawn.dumpSpawnablePlanesList(export_path)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.dumpSpawnablePlanesList(export_path=%s)", export_path)

    local jsonify = function(key, value)
        veaf.loggers.get(veafSpawn.Id):trace(string.format("jsonify(%s)", veaf.p(value)))
        if veaf.json then
            return veaf.json.stringify(value)
        else
            return ""
        end
    end

    -- sort the spawnable planes alphabetically
    local sortedSpawnablePlanesNames = {}
    for _, spawnablePlane in pairs(veafSpawn.airUnitTemplates) do
        local _name = spawnablePlane:getName():sub(veafSpawn.AirUnitTemplatesPrefix:len()+1)
        table.insert(sortedSpawnablePlanesNames, _name)
    end
    table.sort(sortedSpawnablePlanesNames)
    veaf.loggers.get(veafSpawn.Id):trace("sortedSpawnablePlanesNames=%s", veaf.p(sortedSpawnablePlanesNames))

    local _filename = "SpawnablePlanes.json"
    if veaf.config.MISSION_NAME then
        _filename = "SpawnablePlanesList_" .. veaf.config.MISSION_NAME .. ".json"
    end
    if not(veaf.DO_NOT_EXPORT_JSON_FILES) then
        veaf.exportAsJson(sortedSpawnablePlanesNames, "spawnablePlanes", jsonify, _filename, export_path or veaf.config.MISSION_EXPORT_PATH)
    end
end

function veafSpawn.spawnAFAC(spawnSpot, name, country, altitude, speed, hdg, frequency, mod, code, immortal, silent, hiddenOnMFD)

    local coalition = veaf.getCoalitionForCountry(country, true)
    if not coalition then
        veaf.loggers.get(veafSpawn.Id):error("No country/coalition for AFAC !")
        return nil
    end

    -- find template amongst the existing templates (name can be a regex)
    local groupName = veafSpawn.findSpawnableAircraftGroupname(name)
    if not groupName then
        local message = string.format("The AFAC aircraft template could not be found for \"%s\"", veaf.p(name))
        veaf.loggers.get(veafSpawn.Id):info(message)
        trigger.action.outTextForCoalition(coalition, message, 15)
        return nil
    end
    veaf.loggers.get(veafSpawn.Id):trace("found template=%s",groupName)

    if not veafSpawn.AFAC.numberSpawned[coalition] then
        veafSpawn.AFAC.numberSpawned[coalition] = 1
    elseif veafSpawn.AFAC.numberSpawned[coalition] > veafSpawn.AFAC.maximumAmount then
        veaf.loggers.get(veafSpawn.Id):info("The limit for AFACs was reached, one needs to be destroyed")
        if not silent then trigger.action.outTextForCoalition(coalition, "The limit for AFACs was reached, one needs to be destroyed", 15) end
        return false
    end

    veaf.loggers.get(veafSpawn.Id):info(string.format("number of AFAC spawned : %s", veaf.p(veafSpawn.AFAC.numberSpawned[coalition])))

    local AFAC_num = veafSpawn.AFAC.numberSpawned[coalition]
    local newGroupName = veafSpawn.AFAC.callsigns[coalition][AFAC_num].name
    for i = 1, veafSpawn.AFAC.maximumAmount do
        if veafSpawn.AFAC.callsigns[coalition][i].taken == false then
            newGroupName = veafSpawn.AFAC.callsigns[coalition][i].name
            AFAC_num = i
            break
        end
    end
    veaf.loggers.get(veafSpawn.Id):trace("newGroupName=%s",newGroupName)
    veaf.loggers.get(veafSpawn.Id):trace("AFAC_num=%s",AFAC_num)
    veaf.loggers.get(veafSpawn.Id):trace("AFAC coalition=%s",coalition)

    --essentially the same counter but for the template group itself, not for all AFACs
    if not veafSpawn.spawnedNamesIndex[groupName] then
        veafSpawn.spawnedNamesIndex[groupName] = 1
    end

    local codeDigit = {}
    codeDigit = veaf.laserCodeToDigit(code)

    local altitude = altitude or 15000
    if altitude <= 8000 then
        altitude = 15000 -- ft
    end

    local speed = speed or 150 -- kn
    -- convert speed to m/s
    speed = speed/1.94384

    -- convert altitude to meters
    altitude = altitude * 0.3048 -- meters

    --convert heading to radians
    if hdg then
        hdg = hdg * math.pi / 180
    else
        hdg = 0
    end

    local distanceFromTeleport = 3000 --distance between the orbit point and the teleport point in meters

    --calculate DCS radio frequency based on which AFAC out of 8 this is
    local dcsFrequency = veafSpawn.AFAC.baseAFACfrequency[coalition]+(AFAC_num-1)*50000 -- .05 MHz increments

    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", veaf.p(spawnSpot))
    veaf.loggers.get(veafSpawn.Id):trace("name=%s", veaf.p(name))
    veaf.loggers.get(veafSpawn.Id):trace("country=%s", veaf.p(country))
    veaf.loggers.get(veafSpawn.Id):trace("altitude (m)=%s", veaf.p(altitude))
    veaf.loggers.get(veafSpawn.Id):trace("speed (m/s)=%s", veaf.p(speed))
    veaf.loggers.get(veafSpawn.Id):trace("frequency=%s", veaf.p(frequency))
    veaf.loggers.get(veafSpawn.Id):trace("dcsFrequency=%s", veaf.p(dcsFrequency))
    veaf.loggers.get(veafSpawn.Id):trace("code=%s", veaf.p(code))
    veaf.loggers.get(veafSpawn.Id):trace("mod=%s", veaf.p(mod))
    veaf.loggers.get(veafSpawn.Id):trace("silent=%s", veaf.p(silent))
    veaf.loggers.get(veafSpawn.Id):trace("hiddenOnMFD=%s", veaf.p(hiddenOnMFD))

    local teleportSpot = {}
    teleportSpot.x = spawnSpot.x - distanceFromTeleport*math.cos(hdg) --teleport spot is 3km south of the orbit point
    teleportSpot.y = spawnSpot.z - distanceFromTeleport*math.sin(hdg)
    teleportSpot.alt = altitude
    teleportSpot.speed = speed

    --define 2 point route + teleport Waypoint
    local WP = {}
    WP.one = {}
    WP.two = {}
    WP.three = {}
    WP.one.x = teleportSpot.x
    WP.one.y = teleportSpot.y
    WP.two.x = spawnSpot.x - distanceFromTeleport*math.cos(hdg)/2
    WP.two.y = spawnSpot.z - distanceFromTeleport*math.sin(hdg)/2
    WP.three.x = spawnSpot.x
    WP.three.y = spawnSpot.z

    veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "AFAC", "teleportPoint", WP.one)
    veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "AFAC", "setupPoint", WP.two)
    veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "AFAC", "orbitPoint", WP.three)

	local newRoute = {
        ["points"] = {
            -- first point
            [1] = {
                ["type"] = "Turning Point",
                ["action"] = "Turning Point",
                ["x"] = WP.two.x, --1500m south of the orbit point
                ["y"] = WP.two.y,
                ["alt"] = altitude, -- in meters
                ["alt_type"] = "BARO",
                ["speed"] = speed,  -- speed in m/s
                ["speed_locked"] = true,
                ["task"] = {
                    ["id"] = "ComboTask",
                    ["params"] = {
                        ["tasks"] = {
                            [1] = {
                                ["id"]="FAC",
                                ["params"] = {
                                    ["frequency"]=dcsFrequency,
                                    ["modulation"]=0, --0 is AM, 1 is FM
                                    ["callname"]=AFAC_num,
                                    ["number"]=7+coalition, --number x as in it's callsign Springfield x-1 for example
                                    ["priority"]=0,
                                }
                            } -- end of [1]
                        } -- end of tasks
                    } -- end of params
                } -- end of task
            }, -- end of waypoint 1
            [2] = {
                ["type"] = "Turning Point",
                ["action"] = "Turning Point",
                ["x"] = WP.three.x,
                ["y"] = WP.three.y,
                ["alt"] = altitude, -- in meters
                ["alt_type"] = "BARO",
                ["speed"] = speed,  -- speed in m/s
                ["speed_locked"] = true,
                ["task"] = {
                    ["id"] = "ComboTask",
                    ["params"] = {
                        ["tasks"] = {
                            [1] = {
                                ["id"] = "Orbit",
                                ["params"] =
                                {
                                    ["altitude"] = altitude, -- in meters,
                                    ["pattern"] = "Circle",
                                    ["speed"] = speed,  -- speed in m/s
                                }, -- end of ["params"]
                            } -- end of [1]
                        } -- end of ["tasks"]
                    } -- end of ["params"]
                } -- end of ["task"]
            } -- end of waypoint 2
        }
    }

    -- (re)spawn group
    local vars = {}
    vars.gpName = groupName
    vars.name = groupName
    --vars.groupData = _template:getGroupData()
    --replace the callsign to prevent interractions
    vars.route = newRoute
    vars.action = 'clone'
    vars.point = teleportSpot
    vars.newGroupName = newGroupName

    local newGroup = mist.teleportToPoint(vars, true)
    if not newGroup then
        veaf.loggers.get(veafSpawn.Id):error("cannot respawn group %s",veaf.p(vars.name))
        return nil
    end
    if country and #country > 0 then
        newGroup.coalition = coalition
        newGroup.countryId = veaf.getCountryId(country)
    end
    --newGroup.task = "AFAC"
    veaf.loggers.get(veafSpawn.Id):trace("newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"}))

    --setup of the new group
    local unit = newGroup.units[1]
    if not unit then
        veaf.loggers.get(veafSpawn.Id):error("cannot get first unit of group %s",veaf.p(newGroup:getName()))
        return nil
    end

    unit.skill = "Excellent"
    newGroup.hidden=false
    newGroup.name = newGroupName
    newGroup.hiddenOnMFD = hiddenOnMFD

    local unitName = newGroupName
    veaf.loggers.get(veafSpawn.Id):trace("unitName=%s",unitName)
    unit.unitName = unitName
    unit.name = unitName
    newGroup.sameName = true

    unit.alt = teleportSpot.alt

    veaf.loggers.get(veafSpawn.Id):trace("newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"}))
    local _spawnedGroup = mist.dynAdd(newGroup)

    if _spawnedGroup then
        veaf.loggers.get(veafSpawn.Id):trace("_spawnedGroup=%s", veaf.p(_spawnedGroup, nil, {"route", "payload"}))
        veaf.loggers.get(veafSpawn.Id):trace("_spawnedGroup.name=%s",_spawnedGroup.name)
        --mist.goRoute(_spawnedGroup.name, newRoute)

        _spawnedGroup.category = "AIRPLANE"
        _spawnedGroup.country = country
        veaf.loggers.get(veafSpawn.Id):trace("_spawnedGroup=%s", veaf.p(_spawnedGroup))
        veafSpawn.AFAC.missionData[coalition][AFAC_num] = _spawnedGroup --since MIST does not store cloned group data, this is a bit of trickery to allow teleporting AFACs

        -- start lasing 
        if ctld then
            ctld.cleanupJTAC(_spawnedGroup.name)
            local radioData = {freq=frequency, mod=mod, name=_spawnedGroup.name}
            veafSpawn.JTACAutoLase(_spawnedGroup.name, code, radioData)
        end

        local humanFrequency = dcsFrequency/1000000
        local text = "AFAC " .. string.format(veafSpawn.AFAC.numberSpawned[coalition]) .. "/" .. string.format(veafSpawn.AFAC.maximumAmount) .. " - " .. string.format(_spawnedGroup.name) .. " (" .. string.format(country) .. ") - on " .. string.format(humanFrequency) .. "AM (DCS AFAC) or " .. string.format(frequency) .. string.upper(mod) .. " (SRS)"
        veaf.loggers.get(veafSpawn.Id):info(text)
        if not silent then trigger.action.outTextForCoalition(coalition, text, 15) end

        local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name)
        local controller = _dcsSpawnedGroup:getController()

        if immortal then
            veaf.loggers.get(veafSpawn.Id):trace("AFAC immortalized")
            -- JTAC needs to be invisible and immortal
            local _setImmortal = {
                id = 'SetImmortal',
                params = {
                    value = true
                }
            }
            -- invisible to AI, Shagrat
            local _setInvisible = {
                id = 'SetInvisible',
                params = {
                    value = true
                }
            }

            Controller.setCommand(controller, _setImmortal)
            Controller.setCommand(controller, _setInvisible)
        end

        --set the callsign to avoid desyncs in the DCS JTAC menu
        local _setCallsign = {
            id = 'SetCallsign',
            params = {
            callname = AFAC_num,
            number = 9,
            }
        }

        Controller.setCommand(controller, _setCallsign)

        if veafNamedPoints and not silent then
            text = "AFAC" .. " - " .. string.format(_spawnedGroup.name) .. " - " .. string.format(humanFrequency) .. "AM (DCS) or " .. string.format(frequency) .. string.upper(mod) .. " (SRS)"
            veafNamedPoints.namePoint({x=spawnSpot.x, y=altitude, z=spawnSpot.z}, text, veaf.getCoalitionForCountry(country, true), true)
        end

        veafSpawn.afacWatchdog(newGroupName, AFAC_num, coalition, text)
        veafSpawn.AFAC.callsigns[coalition][AFAC_num].taken = true
        veafSpawn.spawnedNamesIndex[groupName] = veafSpawn.spawnedNamesIndex[groupName] + 1
        veafSpawn.AFAC.numberSpawned[coalition] = veafSpawn.AFAC.numberSpawned[coalition] + 1

        return _spawnedGroup.name
    else
        veaf.loggers.get(veafSpawn.Id):error("MIST could not add AFAC")
        return nil
    end
end

function veafSpawn.afacWatchdog(afacGroupName, AFAC_num, coalition, markName)
    if afacGroupName and not Group.getByName(afacGroupName) then
        veaf.loggers.get(veafSpawn.Id):info(string.format("AFAC named=%s is KIA, removing mark (if it exists) and allowing it to be spawned again", veaf.p(afacGroupName)))
        veaf.loggers.get(veafSpawn.Id):trace(string.format("markName=%s", veaf.p(markName)))

        if veafNamedPoints and markName then
            local existingPoint = veafNamedPoints.getPoint(markName)
            veaf.loggers.get(veafSpawn.Id):trace(string.format("existingPoint=%s", veaf.p(existingPoint)))
            if existingPoint and existingPoint.markerId then
                -- delete the existing point
                trigger.action.removeMark(existingPoint.markerId)
            end
        end

        --Make the callsign index available again for spawn
        veaf.loggers.get(veafSpawn.Id):trace(string.format("AFAC_num=%s", veaf.p(AFAC_num)))
        veafSpawn.AFAC.callsigns[coalition][AFAC_num].taken = false
        veafSpawn.AFAC.numberSpawned[coalition] = veafSpawn.AFAC.numberSpawned[coalition] - 1
        mist.DBs.unitsByName[afacGroupName] = nil --MIST does not do it on it's own, I highly recommend looking for an alternative, this is to spawn the AFAC once again with the unit name equal to the group name
        mist.DBs.groupsByName[afacGroupName] = nil
        veafSpawn.AFAC.missionData[coalition][AFAC_num] = nil
    else
        veaf.loggers.get(veafSpawn.Id):trace(string.format("AFAC named=%s is alive", veaf.p(afacGroupName)))

        --update the mark if the AFAC moves
        if veafNamedPoints and markName then
            local existingPoint = veafNamedPoints.getPoint(markName)
            veaf.loggers.get(veafSpawn.Id):trace(string.format("existingAFACmarker=%s", veaf.p(existingPoint)))
            if existingPoint and existingPoint.markerId then
                local AFAC_points = veafSpawn.AFAC.missionData[coalition][AFAC_num].route.points
                local orbitPoint = AFAC_points[#AFAC_points]
                if existingPoint.x ~= orbitPoint.x and existingPoint.z ~= orbitPoint.y then
                    -- delete the existing point
                    veaf.loggers.get(veafSpawn.Id):trace(string.format("Marker needs updating, AFAC moved, newAFACmarker=%s", veaf.p(orbitPoint)))
                    trigger.action.removeMark(existingPoint.markerId)
                    veafNamedPoints.namePoint({x=orbitPoint.x, y=orbitPoint.alt, z=orbitPoint.y}, markName, coalition, true)
                end
            end
        end

        mist.scheduleFunction(veafSpawn.afacWatchdog, {afacGroupName, AFAC_num, coalition, markName}, timer.getTime()+120)
    end
end

function veafSpawn.findSpawnableAircraftGroupname(name)
    -- find template amongst the existing templates (name can be a regex)
    local nameUpper = (name or ""):upper()
    local regexNameUpper = ".*" .. (nameUpper or ".*") .. ".*"
    if not name then
        regexNameUpper = ".*"
    end
    local escapedNameUpper = veaf.escapeRegex(nameUpper)
    veaf.loggers.get(veafSpawn.Id):trace("nameUpper=%s", veaf.p(nameUpper))
    local templatesNamesToChooseFrom = {}
    local chosenTemplateName = nil
    for templateNameUpper, templateData in pairs(veafSpawn.airUnitTemplates) do
        veaf.loggers.get(veafSpawn.Id):trace("templateNameUpper=%s", veaf.p(templateNameUpper))
        if templateNameUpper:match(regexNameUpper) or templateNameUpper:match(escapedNameUpper) then
            local templateName = templateData.name
            veaf.loggers.get(veafSpawn.Id):trace("templateName=%s", veaf.p(templateName))
            table.insert(templatesNamesToChooseFrom, templateName)
        end
    end
    if templatesNamesToChooseFrom and #templatesNamesToChooseFrom > 0 then
        chosenTemplateName = veaf.randomlyChooseFrom(templatesNamesToChooseFrom)
    else
        local message = string.format("The CAP aircraft template could not be found for \"%s\"", veaf.p(name))
        veaf.loggers.get(veafSpawn.Id):info(message)
        trigger.action.outText(message, 15)
        return nil
    end
    veaf.loggers.get(veafSpawn.Id):trace("templatesNamesToChooseFrom=%s", veaf.p(templatesNamesToChooseFrom))
    local chosenTemplateData = veaf.getGroupData(chosenTemplateName)
    veaf.loggers.get(veafSpawn.Id):trace("found template=%s",chosenTemplateData)
    return chosenTemplateName, chosenTemplateData
end

function veafSpawn.spawnCombatAirPatrol(spawnSpot, radius, name, country, altitude, altitudeDelta, hdg, distance, speed, capRadius, skill, silent, hiddenOnMFD)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.spawnCombatAirPatrol(name=%s)", name)

    -- for compatibility reasons we still have altitude and altitudedelta set to zero by default
    if altitude == 0 then altitude = nil end
    if altitudeDelta == 0 then altitudeDelta = nil end

    local coalition = veaf.getCoalitionForCountry(country, true)
    if not coalition then
        veaf.loggers.get(veafSpawn.Id):error("No country/coalition for CAP !")
        return nil
    end

    -- find template amongst the existing templates (name can be a regex)
    local chosenTemplateName, chosenTemplateData = veafSpawn.findSpawnableAircraftGroupname(name)
    if not chosenTemplateName or not chosenTemplateData then
        veaf.loggers.get(veafSpawn.Id):error("spawnCombatAirPatrol: could not find a template for %s", veaf.p(name))
        return
    end

    local function convertSpeeds(speed, mach, altitude)
        local result = speed
        if not result then
            -- compute ground speed in m/s based on MACH and altitude
            result = veaf.convertMachSpeed(0.3, altitude).TAS_ms
        else
            -- compute ground speed in m/s based on IAS and altitude
            result = veaf.convertIndicatedAirSpeed(speed, altitude).TAS_ms
        end
        return result
    end

    local radius = radius or 5000 -- m
    local altitude = (altitude or 27000) --[[ ft ]] * 0.3048 --[[ meters ]]
    local altitudeDelta = (altitudeDelta or 2000) --[[ ft ]] * 0.3048 --[[ meters ]]
    local hdg = hdg or 0
    local speed0 = convertSpeeds(speed, 0.3, altitude)
    local speed1 = convertSpeeds(speed, 0.5, altitude)
    local speed2 = convertSpeeds(speed, 0.63, altitude)
    local speed3 = convertSpeeds(speed, 0.63, altitude)
    local distance = (distance or 20) --[[ nm ]] * 1852 --[[ meters ]]
    local capRadius = (capRadius or 60) * 1852 --[[ meters ]]
    local skill = skill or "random"

    veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", veaf.p(spawnSpot))
    veaf.loggers.get(veafSpawn.Id):trace("radius=%s", veaf.p(radius))
    veaf.loggers.get(veafSpawn.Id):trace("name=%s", veaf.p(name))
    veaf.loggers.get(veafSpawn.Id):trace("country=%s", veaf.p(country))
    veaf.loggers.get(veafSpawn.Id):trace("altitude=%s", veaf.p(altitude))
    veaf.loggers.get(veafSpawn.Id):trace("altdelta=%s", veaf.p(altitudeDelta))
    veaf.loggers.get(veafSpawn.Id):trace("hdg=%s", veaf.p(hdg))
    veaf.loggers.get(veafSpawn.Id):trace("distance=%s", veaf.p(distance))
    veaf.loggers.get(veafSpawn.Id):trace("speed0=%s", veaf.p(speed0))
    veaf.loggers.get(veafSpawn.Id):trace("speed1=%s", veaf.p(speed1))
    veaf.loggers.get(veafSpawn.Id):trace("speed2=%s", veaf.p(speed2))
    veaf.loggers.get(veafSpawn.Id):trace("speed3=%s", veaf.p(speed3))
    veaf.loggers.get(veafSpawn.Id):trace("capRadius=%s", veaf.p(capRadius))
    veaf.loggers.get(veafSpawn.Id):trace("skill=%s", veaf.p(skill))
    veaf.loggers.get(veafSpawn.Id):trace("silent=%s", veaf.p(silent))
    veaf.loggers.get(veafSpawn.Id):trace("hiddenOnMFD=%s", veaf.p(hiddenOnMFD))

    local getRoute = function(parameters)
        local newRoute = {
            ["points"] = {
                [1] =
                {
                    ["alt"] = parameters.altitude,
                    ["action"] = "Turning Point",
                    ["alt_type"] = "BARO",
                    ["speed"] = parameters.speed1,
                    ["properties"] =
                    {
                        ["addopt"] =
                        {
                        }, -- end of ["addopt"]
                    }, -- end of ["properties"]
                    ["task"] = parameters.chosenTemplateWp1Task,
                    ["type"] = "Turning Point",
                    ["ETA"] = 10000,
                    ["ETA_locked"] = false,
                    ["y"] = parameters.wp1.y,
                    ["x"] = parameters.wp1.x,
                    ["formation_template"] = "",
                    ["speed_locked"] = true,
                }, -- end of [1]
                [2] =
                {
                    ["alt"] = parameters.altitude,
                    ["action"] = "Turning Point",
                    ["alt_type"] = "BARO",
                    ["speed"] = parameters.speed2,
                    ["properties"] =
                    {
                        ["addopt"] =
                        {
                        }, -- end of ["addopt"]
                    }, -- end of ["properties"]
                    ["task"] =
                    {
                        ["id"] = "ComboTask",
                        ["params"] =
                        {
                            ["tasks"] =
                            {
                                -- [1] = 
                                -- {
                                --     ["number"] = 1,
                                --     ["auto"] = false,
                                --     ["enabled"] = true,
                                --     ["id"] = "EngageTargetsInZone",
                                --     ["params"] = {
                                --         ["noTargetTypes"] = {
                                --             [1] = "Cruise missiles",
                                --             [2] = "Antiship Missiles",
                                --             [3] = "AA Missiles",
                                --             [4] = "AG Missiles",
                                --             [5] = "SA Missiles",
                                --         }, -- end of ["noTargetTypes"]
                                --         ["priority"] = 0,
                                --         ["targetTypes"] = {
                                --             [1] = "Air",
                                --         }, -- end of ["targetTypes"]
                                --         ["value"] = "Air;",
                                --         ["x"] = parameters.targetZone.x,
                                --         ["y"] = parameters.targetZone.y,
                                --         ["zoneRadius"] = parameters.targetZone.radius,
                                --     }, -- end of ["params"]
                                -- }, -- end of [1]                       
                            }, -- end of ["tasks"]
                        }, -- end of ["params"]
                    }, -- end of ["task"]
                    ["type"] = "Turning Point",
                    ["ETA"] = 20000,
                    ["ETA_locked"] = false,
                    ["y"] = parameters.wp2.y,
                    ["x"] = parameters.wp2.x,
                    ["formation_template"] = "",
                    ["speed_locked"] = true,
                }, -- end of [2]
                [3] =
                {
                    ["alt"] = parameters.altitude,
                    ["action"] = "Turning Point",
                    ["alt_type"] = "BARO",
                    ["speed"] = parameters.speed3,
                    ["properties"] =
                    {
                        ["addopt"] =
                        {
                        }, -- end of ["addopt"]
                    }, -- end of ["properties"]
                    ["task"] =
                    {
                        ["id"] = "ComboTask",
                        ["params"] =
                        {
                            ["tasks"] =
                            {
                                [1] =
                                {
                                    ["enabled"] = true,
                                    ["auto"] = false,
                                    ["id"] = "WrappedAction",
                                    ["number"] = 1,
                                    ["params"] =
                                    {
                                        ["action"] =
                                        {
                                            ["id"] = "SwitchWaypoint",
                                            ["params"] =
                                            {
                                                ["goToWaypointIndex"] = 2,
                                                ["fromWaypointIndex"] = 3,
                                            }, -- end of ["params"]
                                        }, -- end of ["action"]
                                    }, -- end of ["params"]
                                }, -- end of [1]
                            }, -- end of ["tasks"]
                        }, -- end of ["params"]
                    }, -- end of ["task"]
                    ["type"] = "Turning Point",
                    ["ETA"] = 30000,
                    ["ETA_locked"] = false,
                    ["y"] = parameters.wp3.y,
                    ["x"] = parameters.wp3.x,
                    ["formation_template"] = "",
                    ["speed_locked"] = true,
                }, -- end of [3]
            }
        }

        return newRoute
    end

    -- find spawn spot
    if altitudeDelta then
        altitude = altitude + math.random(0, altitudeDelta*2) - altitudeDelta
    end
    local position = mist.getRandPointInCircle(spawnSpot, radius)
    position.z = position.y
    position.y = altitude
    veaf.loggers.get(veafSpawn.Id):debug("final spawn, position=%s",position)

    -- get the template first waypoint's options
    veaf.loggers.get(veafSpawn.Id):trace("chosenTemplateData=%s", veaf.p(chosenTemplateData))
    local chosenTemplateWp1Task = {}
    if chosenTemplateData then
        local _route = chosenTemplateData.route
        --veaf.loggers.get(veafSpawn.Id):trace("_route=%s", veaf.p(_route))
        if _route then
            local _points = _route.points
            --veaf.loggers.get(veafSpawn.Id):trace("_points=%s", veaf.p(_points))
            if _points then
                local _point1 = _points[1]
                --veaf.loggers.get(veafSpawn.Id):trace("_point1=%s", veaf.p(_point1))
                if _point1 then
                    local _task = _point1.task
                    --veaf.loggers.get(veafSpawn.Id):trace("_task=%s", veaf.p(_task))
                    if _task and "ComboTask" == _task.id then
                        local _params = _task.params
                        --veaf.loggers.get(veafSpawn.Id):trace("_params=%s", veaf.p(_params))
                        if _params then
                            local _tasks = _params.tasks
                            --veaf.loggers.get(veafSpawn.Id):trace("_tasks=%s", veaf.p(_tasks))
                            if _tasks then
                                for _, _taskData in pairs(_tasks) do
                                    if "WrappedAction" == _taskData.id then
                                        table.insert(chosenTemplateWp1Task, mist.utils.deepCopy(_task)) -- if we found a WrappedAction task then we're on the right way, clone the whole task package
                                        break
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
    end
    --veaf.loggers.get(veafSpawn.Id):trace("chosenTemplateWp1Task=%s", veaf.p(chosenTemplateWp1Task))

    -- compute route
    local headingRad = mist.utils.toRadian(hdg)
    local parameters = {
        altitude = altitude,
        speed0 = speed0,
        speed1 = speed1,
        speed2 = speed2,
        speed3 = speed3,
        wp1 = { x = position.x, y = position.z },
        wp1Options = chosenTemplateWp1Task
    }
    parameters.wp2 = { x = parameters.wp1.x + 2500 * math.cos(headingRad), y = parameters.wp1.y + 2500 * math.sin(headingRad) } -- second wp at 2500m in the right direction
    parameters.wp3 = { x = parameters.wp2.x + distance * math.cos(headingRad), y = parameters.wp2.y + distance * math.sin(headingRad) } -- last wp at the right distance in the right direction
    parameters.targetZone = { x = (parameters.wp2.x + parameters.wp3.x) / 2, y = (parameters.wp2.y + parameters.wp3.y) / 2, radius = capRadius } -- target zone at the middle point between wp2 and wp3

    veaf.loggers.get(veafSpawn.Id):trace("to create route, parameters=%s",parameters)
    local newRoute = getRoute(parameters)

    veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "wp1", parameters.wp1)
    veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "wp2", parameters.wp2)
    veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "wp3", parameters.wp3)
    veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "targetZone", parameters.targetZone, nil, capRadius, {1,0,0,0.15})

    if not veafSpawn.spawnedNamesIndex[chosenTemplateName] then
        veafSpawn.spawnedNamesIndex[chosenTemplateName] = 1
    else
        veafSpawn.spawnedNamesIndex[chosenTemplateName] = veafSpawn.spawnedNamesIndex[chosenTemplateName] + 1
    end
    local newGroupName = string.format("%s #%04d", chosenTemplateName, veafSpawn.spawnedNamesIndex[chosenTemplateName])
    veaf.loggers.get(veafSpawn.Id):debug("indexed newGroupName=%s",newGroupName)

    -- (re)spawn group
    local vars = {}
    vars.gpName = chosenTemplateName
    vars.name = chosenTemplateName
    --vars.groupData = _template:getGroupData()
    vars.route = newRoute
    vars.action = 'clone'
    vars.point = position
    vars.newGroupName = newGroupName

    local newGroup = mist.teleportToPoint(vars, true)
    if not newGroup then
        veaf.loggers.get(veafSpawn.Id):error("cannot respawn group %s",veaf.p(vars.name))
        return nil
    end
    if country and #country > 0 then
        newGroup.countryId = veaf.getCountryId(country)
    end
    --newGroup.task = "CAP" --needs to be set in the editor
    veaf.loggers.get(veafSpawn.Id):trace("after preparation by MIST, newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"}))

    newGroup.hidden = false
    newGroup.name = newGroupName
    newGroup.hiddenOnMFD = hiddenOnMFD

    for _, unit in pairs(newGroup.units) do
        unit.skill = skill
        local unitName = unit.unitName or unit.name
        veaf.loggers.get(veafSpawn.Id):trace("original unitName=%s",unitName)
        if not veafSpawn.spawnedNamesIndex[unitName] then
            veafSpawn.spawnedNamesIndex[unitName] = 1
        else
            veafSpawn.spawnedNamesIndex[unitName] = veafSpawn.spawnedNamesIndex[unitName] + 1
        end
        local spawnedUnitName = string.format("%s #%04d", unitName, veafSpawn.spawnedNamesIndex[unitName])
        unit.name = spawnedUnitName
        unit.alt = position.y
        veaf.loggers.get(veafSpawn.Id):debug("indexed spawnedUnitName=%s",spawnedUnitName)
    end

    veaf.loggers.get(veafSpawn.Id):trace("before mist.dynAdd, newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"}))
    local _spawnedGroup = mist.dynAdd(newGroup)
    if not _spawnedGroup then
        veaf.loggers.get(veafSpawn.Id):error("cannot spawn group %s",veaf.p(newGroup.name))
        return nil
    end
    veaf.loggers.get(veafSpawn.Id):debug("after mist.dynAdd, _spawnedGroup.name=%s",_spawnedGroup.name)
    veaf.loggers.get(veafSpawn.Id):trace("after mist.dynAdd, _spawnedGroup=%s", veaf.p(_spawnedGroup, nil, {"route", "payload"}))

    local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name)
    veaf.loggers.get(veafSpawn.Id):trace("result of dcs side getByName, _dcsSpawnedGroup=%s", veaf.p(_dcsSpawnedGroup, nil, {"route", "payload"}))
    veaf.loggers.get(veafSpawn.Id):debug("result of dcs side getByName, _dcsSpawnedGroup.name=%s", _dcsSpawnedGroup:getName())
    for index, unit in pairs(_dcsSpawnedGroup:getUnits()) do
        veaf.loggers.get(veafSpawn.Id):debug("result of dcs side getByName, _dcsSpawnedGroup.unit[%s].name=%s", index, unit:getName())
    end

    local controller = _dcsSpawnedGroup:getController()
    controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true)

    veaf.loggers.get(veafSpawn.Id):debug("starting CAP target watchdog...")
    mist.scheduleFunction(veafSpawn.startCapWatchdog, {_spawnedGroup.name, coalition, parameters.targetZone}, timer.getTime() + 1)

    local message = string.format("A CAP of %s (%s) has been spawned", name, country)
    veaf.loggers.get(veafSpawn.Id):info(message)
    if not silent then trigger.action.outText(message, 15) end

    return _spawnedGroup.name
end

function veafSpawn.startCapWatchdog(capGroupName, capCoalition, capZone, pTargetsList, pNumberOfTasksAddedByWatchdog)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.startCapWatchdog(capGroupName=%s)", veaf.p(capGroupName))
    veaf.loggers.get(veafSpawn.Id):trace("capZone=%s", veaf.p(capZone))

    if capGroupName == nil then
        veaf.loggers.get(veafSpawn.Id):error("veafSpawn.startCapWatchdog; capGroupName is mandatory !")
        return
    end

    if capCoalition == nil then
        veaf.loggers.get(veafSpawn.Id):error("veafSpawn.startCapWatchdog; capCoalition is mandatory !")
        return
    end

    local capGroup = Group.getByName(capGroupName)
    if not capGroup then
        veaf.loggers.get(veafSpawn.Id):debug("CAP group %s is nowhere to be found, stopping watchdog", veaf.p(capGroupName))
        return
    end
    local capGroupPosition = veaf.getAveragePosition(capGroup)
    if not capGroupPosition then
        veaf.loggers.get(veafSpawn.Id):error("CAP group %s has no position!", veaf.p(capGroupName))
        return
    end

    veaf.loggers.get(veafSpawn.Id):trace("Looking in CAP zone for targets...")
    local timestamp = timer.getTime()
    local targetsList = pTargetsList or {}
    local numberOfTasksAddedByWatchdog = pNumberOfTasksAddedByWatchdog or 0
    veaf.loggers.get(veafSpawn.Id):trace("targetsList=%s", veaf.p(targetsList))
    veaf.loggers.get(veafSpawn.Id):trace("numberOfTasksAddedByWatchdog=%s", veaf.p(numberOfTasksAddedByWatchdog))

    -- check CAP group for state and position
    local capLanded = true
    local capInZone = false
    for _,unit in pairs(capGroup:getUnits()) do
        if unit and unit:inAir() then
            capLanded = false
            local isUnitInZone = veaf.isUnitInZone(unit, capZone)
            veaf.loggers.get(veafSpawn.Id):trace("unitName=%s, isUnitInZone=%s", veaf.p(unit:getName()), veaf.p(isUnitInZone))
            if isUnitInZone then
                capInZone = true
                -- unit is in the zone, and in the air, let's test the targets it can see
                local detectedTargets = unit:getController():getDetectedTargets()
                if detectedTargets and #detectedTargets > 0 then
                    -- process each target and compute its priority, then add it to the targets list
                    for _, detectedTarget in pairs(detectedTargets) do
                        local target = detectedTarget.object
                        local targetId = target:getID()
                        local targetGroup = target:getGroup()
                        local targetGroupName = targetGroup:getName()
                        local targetName = target:getName()
                        veaf.loggers.get(veafSpawn.Id):trace("Checking targetGroupName=%s, targetName=%s, targetId=%s", veaf.p(targetGroupName), veaf.p(targetName), veaf.p(targetId))
                        local targetIsAirborne = target:isActive() and target:inAir()
                        local targetCoalition = target:getCoalition()
                        local targetGroupCategory = targetGroup:getCategory()
                        veaf.loggers.get(veafSpawn.Id):trace("targetIsAirborne=%s", veaf.p(targetIsAirborne))
                        veaf.loggers.get(veafSpawn.Id):trace("targetCoalition=%s", veaf.p(targetCoalition))
                        veaf.loggers.get(veafSpawn.Id):trace("targetGroupCategory=%s", veaf.p(targetGroupCategory))

                        if  targetIsAirborne ~= nil and targetGroupCategory ~= nil and targetCoalition ~= nil
                            and targetCoalition ~= capCoalition
                            and (targetGroupCategory == Group.Category.AIRPLANE or targetGroupCategory == Group.Category.HELICOPTER) then

                            local targetPosition = target:getPosition().p
                            local targetDistanceFromCapZoneCenter = mist.utils.get2DDist(targetPosition, capZone)
                            veaf.loggers.get(veafSpawn.Id):trace("targetPosition=%s", veaf.p(targetPosition))
                            veaf.loggers.get(veafSpawn.Id):trace("targetDistanceFromCapZoneCenter=%s", veaf.p(targetDistanceFromCapZoneCenter))
                            if targetDistanceFromCapZoneCenter <= capZone.radius then
                                -- consider only the targets that are in the CAP zone

                                local targetAttributes = target:getDesc().attributes
                                local targetType = target:getTypeName()
                                local targetDistanceFromCapGroup = mist.utils.get2DDist(targetPosition, capGroupPosition)
                                veaf.loggers.get(veafSpawn.Id):trace("targetType=%s", veaf.p(targetType))
                                veaf.loggers.get(veafSpawn.Id):trace("targetAttributes=%s", veaf.p(targetAttributes))
                                veaf.loggers.get(veafSpawn.Id):trace("targetDistanceFromCapGroup=%s", veaf.p(targetDistanceFromCapGroup))

                                local targetPriority = nil

                                if targetAttributes["Fighters"] or targetAttributes["Multirole fighters"] then
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is a Fighter")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/2)
                                elseif targetAttributes["Strategic bombers"] then
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is a strategic bomber")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/1.5) + 10000
                                elseif targetAttributes["Bombers"] then
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is a bomber")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/1) + 15000
                                elseif targetAttributes["UAVs"] and targetType ~= "Yak-52" then --wtf ED, Yak-52 UAV master race
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is a UAV (except the Yak-52, that shit is not a UAV ED)")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000
                                elseif targetAttributes["AWACS"] then
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is an AWACS")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000
                                elseif targetAttributes["Transports"] then
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is a Transport")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000
                                elseif targetAttributes["Battle airplanes"] or targetAttributes["Battleplanes"] then
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is a generic Battleplane")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/0.25) + 15000
                                elseif targetAttributes["Helicopters"] or targetAttributes["Attack helicopters"] or targetAttributes["Transport helicopters"] then
                                    veaf.loggers.get(veafSpawn.Id):trace("Target is a Helicopter")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/0.1) + 20000
                                else
                                    veaf.loggers.get(veafSpawn.Id):trace("Target has unknown attributes, calculating generic priority")
                                    targetPriority = math.floor(targetDistanceFromCapGroup/0.25) + 15000
                                end
                                -- https://www.geogebra.org/calculator if you want to visualize, type in functions y=x/factor + offset and set points on each curve. y is the priority, x the distance

                                veaf.loggers.get(veafSpawn.Id):trace("priority=%s", veaf.p(targetPriority))


                                local targetData = targetsList[targetId]
                                if targetData then
                                    -- this target has already been detected; was it in the same run, by another plane from the CAP group ?
                                    if targetData.seenAt == timestamp then
                                        -- yes, we can only increase the priority (never decrease it)
                                        veaf.loggers.get(veafSpawn.Id):debug("redetection (same run) of targetName=%s", veaf.p(targetName))
                                        if targetData.priority < targetPriority then
                                            veaf.loggers.get(veafSpawn.Id):debug("increasing priority of targetName=%s to %s", veaf.p(targetName), veaf.p(targetPriority))
                                            targetData.priority = targetPriority
                                        end
                                    else
                                        -- no, it's an old target, let's mark it as old
                                        veaf.loggers.get(veafSpawn.Id):debug("redetection (previous run) of targetName=%s", veaf.p(targetName))
                                        targetData.isNew = false
                                    end
                                else
                                    -- new target! register in into the target list
                                    veaf.loggers.get(veafSpawn.Id):debug("new detection of targetName=%s, priority=%s", veaf.p(targetName), veaf.p(targetPriority))
                                    targetsList[targetId] = {isNew = true, seenAt = timestamp, priority = targetPriority, targetId = targetId, unit = unit}
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    if capLanded then
        capGroup:destroy()
        veaf.loggers.get(veafSpawn.Id):debug("CAP group %s is landed, destroying it and stopping watchdog", veaf.p(capGroupName))
        return
    end

    local controller = capGroup:getController()
    if capInZone then
        veaf.loggers.get(veafSpawn.Id):debug("CAP group is still in the CAP zone...")
        if not controller then
            veaf.loggers.get(veafSpawn.Id):error("cannot find controller for CAP group!")
            return
        end

        controller:setOption(AI.Option.Air.id.PROHIBIT_AA, false)
        controller:setOption(0,0) --weapons free
        --sort the list in reverse priority order so that the last task to be pushed in spot #1 is the one with the lowest priority, couldn't quite figure out which way works best, since this one makes the least sense it seems appropriate for DCS
        table.sort(targetsList, function(a,b) return a.priority < b.priority end)
        veaf.loggers.get(veafSpawn.Id):trace("targetsList=%s", veaf.p(targetsList))
        local foundTargets = false
        for targetId, targetData in pairs(targetsList) do
            if not foundTargets then
                -- only write that once!
                veaf.loggers.get(veafSpawn.Id):debug("Watchdog has targets ! Allowing AA for CAP")
                foundTargets = true
            end
            if not Unit.isExist(targetData.unit) or not targetData.unit:inAir() or timestamp > targetData.seenAt + veafSpawn.CAP_WATCHDOG_DELAY*2 then
                veaf.loggers.get(veafSpawn.Id):trace("Target is outdated, landed or doesn't exist, removing it from the list")
                targetsList[targetId] = nil
            else
                veaf.loggers.get(veafSpawn.Id):trace("Engaging target!")
                local engageUnit = {
                    id = 'EngageUnit',
                    params = {
                        unitId = targetId,
                        weaponType = "ALL",
                        priority = targetData.priority,
                    }
                }
                controller:pushTask(engageUnit)
                numberOfTasksAddedByWatchdog = numberOfTasksAddedByWatchdog + 1
            end
        end

        if not foundTargets then
            -- no targets, let's remove all the tasks we added (taking care not to remove the original task, which is to fly along the route)
            veaf.loggers.get(veafSpawn.Id):debug("Watchdog found no targets, removing all tasks and prohibiting AA for CAP")
            while controller:hasTask() and numberOfTasksAddedByWatchdog > 0 do
                veaf.loggers.get(veafSpawn.Id):trace("numberOfTasksAddedByWatchdog=%s", veaf.p(numberOfTasksAddedByWatchdog))
                controller:resetTask()
                veaf.loggers.get(veafSpawn.Id):trace("resetTask() called")
                numberOfTasksAddedByWatchdog = numberOfTasksAddedByWatchdog - 1
            end
            controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true)
            controller:setOption(0,3) --return fire
        end
    else
        veaf.loggers.get(veafSpawn.Id):debug("CAP is outside of its area ! Discarding targets...")
        controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true)
        controller:setOption(0,3) --return fire
    end

    veaf.loggers.get(veafSpawn.Id):debug(string.format("Rescheduling watchdog in %s seconds", veafSpawn.CAP_WATCHDOG_DELAY))
    veaf.loggers.get(veafSpawn.Id):debug("===============================================================================")
    mist.scheduleFunction(veafSpawn.startCapWatchdog, {capGroupName, capCoalition, capZone, targetsList, numberOfTasksAddedByWatchdog}, timer.getTime() + veafSpawn.CAP_WATCHDOG_DELAY)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mission master features
-------------------------------------------------------------------------------------------------------------------------------------------------------------
veafSpawn.missionMasterRunnables = {}
veafSpawn.missionMasterRunnables.__silent = true

function veafSpawn.missionMasterSetMessagingMode(silent, toGroupId)
    veafSpawn.missionMasterRunnables.__silent = silent
    veafSpawn.missionMasterRunnables.__toGroupId = toGroupId
end

function veafSpawn.missionMasterOutText(message)
    -- don't send the message if __silent is true
    if not(veafSpawn.missionMasterRunnables.__silent) then
        if (veafSpawn.missionMasterRunnables.__toGroupId) then
            -- send to a group
            trigger.action.outTextForGroup(veafSpawn.missionMasterRunnables.__toGroupId, message, 5)
        else
            -- send to all
            trigger.action.outText(message, 5)
        end
    end
end

function veafSpawn.missionMasterAddRunnable(name, code, parameters)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterAddRunnable(name=%s)",name)
    veafSpawn.missionMasterRunnables[veaf.ifnn(name, "upper")] = { code, parameters }
end

function veafSpawn.missionMasterRun(name)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterRun(name=%s)",name)
    if not name or #name == 0 then
        local message = "Mission Master, `run` requires the name of the code to be run"
        veaf.loggers.get(veafSpawn.Id):warn(message)
        veafSpawn.missionMasterOutText(message)
        return
    end

    local code, parameters = veaf.safeUnpack(veafSpawn.missionMasterRunnables[veaf.ifnn(name, "upper")])
    if code then
        local sta, res = pcall(code, parameters)
        if sta then
            local message = string.format("Mission Master, the runnable [%s] was successfully run and returned : %s", name, veaf.p(res))
            veaf.loggers.get(veafSpawn.Id):warn(message)
            veafSpawn.missionMasterOutText(message)
        else
            local message = string.format("Mission Master, the runnable [%s] returned an error : %s", name, veaf.p(res))
            veaf.loggers.get(veafSpawn.Id):warn(message)
            veafSpawn.missionMasterOutText(message)
        end
    else
        local message = string.format("Mission Master, the runnable [%s] does not exist", name)
        veaf.loggers.get(veafSpawn.Id):warn(message)
        veafSpawn.missionMasterOutText(message)
    end
end

function veafSpawn.missionMasterSetFlagFromTable(parameters)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterSetFlagFromTable(parameters=%s)", parameters)
    local name, value = veaf.safeUnpack(parameters)
    return veafSpawn.missionMasterSetFlag(name, value)
end

function veafSpawn.missionMasterIncrementFlagValue(name)
    veafSpawn.missionMasterAddValueToFlag(name, 1)
end

function veafSpawn.missionMasterDecrementFlagValue(name)
    veafSpawn.missionMasterAddValueToFlag(name, -1)
end

function veafSpawn.missionMasterAddValueToFlag(name, increment)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterIncrementFlagValue(name=%s, increment=%s)", name, increment)
    if not name then
        local message = "Mission Master, `setFlag` requires the name or number of the flag"
        veaf.loggers.get(veafSpawn.Id):warn(message)
        veafSpawn.missionMasterOutText(message)
        return
    end
    local value = trigger.misc.getUserFlag(name)
    if not value then value = 0 end
    trigger.action.setUserFlag(name , value + increment)
end

function veafSpawn.missionMasterSetFlag(name, value)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterSetFlag(name=%s, value=%s)", name, value)
    if not name then
        local message = "Mission Master, `setFlag` requires the name or number of the flag"
        veaf.loggers.get(veafSpawn.Id):warn(message)
        veafSpawn.missionMasterOutText(message)
        return
    end
    trigger.action.setUserFlag(name , value)
end

function veafSpawn.missionMasterGetFlag(name)
    veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterGetFlag(name=%s)", name)
    if not name then
        local message = "Mission Master, `getFlag` requires the name or number of the flag"
        veaf.loggers.get(veafSpawn.Id):warn(message)
        veafSpawn.missionMasterOutText(message)
        return
    end
    local value = trigger.misc.getUserFlag(name)
    local message = string.format("Mission Master, flag [%s] has value [%s]", name, veaf.p(value))
    veaf.loggers.get(veafSpawn.Id):info(message)
    veafSpawn.missionMasterOutText(message)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Radio menu
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Build the initial radio menu
function veafSpawn.buildRadioMenu()
    veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.buildRadioMenu() hideMenu%s", veaf.p(veafSpawn.HideRadioMenu)))
    if not veafSpawn.HideRadioMenu then
        veafSpawn.rootPath = veafRadio.addSubMenu(veafSpawn.RadioMenuName)
        veafRadio.addCommandToSubmenu("Available Aircraft spawns", veafSpawn.rootPath, veafSpawn.listAllCAP, nil, veafRadio.USAGE_ForAll)
        veafRadio.addCommandToSubmenu("Info on all convoys", veafSpawn.rootPath, veafSpawn.infoOnAllConvoys, nil, veafRadio.USAGE_ForGroup)
        local menuPath = veafRadio.addSubMenu("Mark closest convoy route", veafSpawn.rootPath)
        veafRadio.addCommandToSubmenu("Mark closest convoy route" , menuPath, veafSpawn.markClosestConvoyRouteWithSmoke, nil, veafRadio.USAGE_ForGroup)
        local menuPath = veafRadio.addSubMenu("Mark closest convoy", veafSpawn.rootPath)
        veafRadio.addCommandToSubmenu("Mark closest convoy" , menuPath, veafSpawn.markClosestConvoyWithSmoke, nil, veafRadio.USAGE_ForGroup)
        local menuPath = veafRadio.addSubMenu("Stop closest convoy", veafSpawn.rootPath)
        veafRadio.addCommandToSubmenu("Stop closest convoy" , menuPath, veafSpawn.stopClosestConvoy, nil, veafRadio.USAGE_ForGroup)
        local menuPath = veafRadio.addSubMenu("Makes closest convoy move", veafSpawn.rootPath)
        veafRadio.addCommandToSubmenu("Make closest convoy move" , menuPath, veafSpawn.moveClosestConvoy, nil, veafRadio.USAGE_ForGroup)
        veafRadio.addSecuredCommandToSubmenu('Cleanup all convoys', veafSpawn.rootPath, veafSpawn.cleanupAllConvoys)
        veafRadio.refreshRadioMenu()
    end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- initialisation
-------------------------------------------------------------------------------------------------------------------------------------------------------------

function veafSpawn.initialize()
    veafSpawn.buildRadioMenu()
    veafSpawn.initializeAirUnitTemplates()
    veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafSpawn.onEventMarkChange)
    veafSpawn.dumpSpawnablePlanesList()
end

veaf.loggers.get(veafSpawn.Id):info(string.format("Loading version %s", veafSpawn.Version))