--[[
Pretense Dynamic Mission Engine
## Description:

Pretense Dynamic Mission Engine (PDME) is a the heart and soul of the Pretense missions.
You are allowed to use and modify this script for personal or private use.
Please do not share modified versions of this script.
Please do not reupload missions that use this script.
Please do not charge money for access to missions using this script.

## Links:

ED Forums Post: <https://forum.dcs.world/topic/327483-pretense-dynamic-campaign>

Pretense Manual: <https://github.com/Dzsek/pretense>

If you'd like to buy me a beer: <https://www.buymeacoffee.com/dzsek>

Makes use of Mission scripting tools (Mist): <https://github.com/mrSkortch/MissionScriptingTools>

@script PDME
@author Dzsekeb

]]--

-----------------[[ GroupCorrection.lua ]]-----------------

Group.getByNameBase = Group.getByName

function Group.getByName(name)
    local g = Group.getByNameBase(name)
    if not g then return nil end
    if g:getSize() == 0 then return nil end
    return g
end

-----------------[[ END OF GroupCorrection.lua ]]-----------------



-----------------[[ DependencyManager.lua ]]-----------------

DependencyManager = {}

do
    DependencyManager.dependencies = {}

    function DependencyManager.register(name, dependency)
        DependencyManager.dependencies[name] = dependency
        env.info("DependencyManager - "..name.." registered")
    end

    function DependencyManager.get(name)
        return DependencyManager.dependencies[name]
    end
end

-----------------[[ END OF DependencyManager.lua ]]-----------------



-----------------[[ Config.lua ]]-----------------

Config = Config or {}
Config.lossCompensation = Config.lossCompensation or 1.1 -- gives advantage to the side with less zones. Set to 0 to disable
Config.randomBoost = Config.randomBoost or 0.0004 -- adds a random factor to build speeds that changes every 30 minutes, set to 0 to disable
Config.buildSpeed = Config.buildSpeed or 10 -- structure and defense build speed
Config.supplyBuildSpeed = Config.supplyBuildSpeed or 85 -- supply helicopters and convoys build speed
Config.missionBuildSpeedReduction = Config.missionBuildSpeedReduction or 0.12 -- reduction of build speed in case of ai missions
Config.maxDistFromFront = Config.maxDistFromFront or 129640 -- max distance in meters from front after which zone is forced into low activity state (export mode)

if Config.restrictMissionAcceptance == nil then Config.restrictMissionAcceptance = true end -- if set to true, missions can only be accepted while landed inside friendly zones

Config.missions = Config.missions or {}
Config.missionBoardSize = Config.missionBoardSize or 15

Config.carrierSpawnCost = Config.carrierSpawnCost or 500 -- resource cost for carrier when players take off, set to 0 to disable restriction
Config.zoneSpawnCost = Config.zoneSpawnCost or 500 -- resource cost for zones when players take off, set to 0 to disable restriction

-----------------[[ END OF Config.lua ]]-----------------



-----------------[[ Utils.lua ]]-----------------

Utils = {}
do
	local JSON = (loadfile('Scripts/JSON.lua'))()

	function Utils.getPointOnSurface(point)
		return {x = point.x, y = land.getHeight({x = point.x, y = point.z}), z= point.z}
	end
	
	function Utils.getTableSize(tbl)
		local cnt = 0
		for i,v in pairs(tbl) do cnt=cnt+1 end
		return cnt
	end

	function Utils.isInArray(value, array)
		for _,v in ipairs(array) do
			if value == v then
				return true
			end
		end
	end

	Utils.cache = {}
	Utils.cache.groups = {}
	function Utils.getOriginalGroup(groupName)
		if Utils.cache.groups[groupName] then
			return Utils.cache.groups[groupName]
		end

		for _,coalition in pairs(env.mission.coalition) do
			for _,country in pairs(coalition.country) do
				local tocheck = {}
				table.insert(tocheck, country.plane)
				table.insert(tocheck, country.helicopter)
				table.insert(tocheck, country.ship)
				table.insert(tocheck, country.vehicle)
				table.insert(tocheck, country.static)

				for _, checkGroup in ipairs(tocheck) do
					for _,item in pairs(checkGroup.group) do
						Utils.cache.groups[item.name] = item
						if item.name == groupName then
							return item
						end
					end
				end
			end
		end
	end
	
	function Utils.getBearing(fromvec, tovec)
		local fx = fromvec.x
		local fy = fromvec.z
		
		local tx = tovec.x
		local ty = tovec.z
		
		local brg = math.atan2(ty - fy, tx - fx)
		
		
		if brg < 0 then
			 brg = brg + 2 * math.pi
		end
		
		brg = brg * 180 / math.pi
		

		return brg
	end

	function Utils.getHeadingDiff(heading1, heading2) -- heading1 + result == heading2
		local diff = heading1 - heading2
		local absDiff = math.abs(diff)
		local complementaryAngle = 360 - absDiff
	
		if absDiff <= 180 then 
			return -diff
		elseif heading1 > heading2 then
			return complementaryAngle
		else
			return -complementaryAngle
		end
	end
	
	function Utils.getAGL(object)
		local pt = object:getPoint()
		return pt.y - land.getHeight({ x = pt.x, y = pt.z })
	end

	function Utils.round(number)
		return math.floor(number+0.5)
	end
	
	function Utils.isLanded(unit, ignorespeed)
		--return (Utils.getAGL(unit)<5 and mist.vec.mag(unit:getVelocity())<0.10)
		
		if ignorespeed then
			return not unit:inAir()
		else
			return (not unit:inAir() and mist.vec.mag(unit:getVelocity())<1)
		end
	end

	function Utils.getEnemy(ofside)
		if ofside == 1 then return 2 end
		if ofside == 2 then return 1 end
	end
	
	function Utils.isGroupActive(group)
		if group and group:getSize()>0 and group:getController():hasTask() then 
			return not Utils.allGroupIsLanded(group, true)
		else
			return false
		end
	end
	
	function Utils.isInAir(unit)
		--return Utils.getAGL(unit)>5
		return unit:inAir()
	end
	
	function Utils.isInZone(unit, zonename)
		local zn = CustomZone:getByName(zonename)
		if zn then
			return zn:isInside(unit:getPosition().p)
		end
		
		return false
	end

	function Utils.isInCircle(point, center, radius)
		local dist = mist.utils.get2DDist(point, center)
		return dist<radius
	end
	
	function Utils.isCrateSettledInZone(crate, zonename)
		local zn = CustomZone:getByName(zonename)
		if zn and crate then
			return (zn:isInside(crate:getPosition().p) and Utils.getAGL(crate)<1)
		end
		
		return false
	end
	
	function Utils.someOfGroupInZone(group, zonename)
		for i,v in pairs(group:getUnits()) do
			if Utils.isInZone(v, zonename) then
				return true
			end
		end
		
		return false
	end
	
	function Utils.allGroupIsLanded(group, ignorespeed)
		for i,v in pairs(group:getUnits()) do
			if not Utils.isLanded(v, ignorespeed) then
				return false
			end
		end
		
		return true
	end
	
	function Utils.someOfGroupInAir(group)
		for i,v in pairs(group:getUnits()) do
			if Utils.isInAir(v) then
				return true
			end
		end
		
		return false
	end
	
	Utils.canAccessFS = true
	function Utils.saveTable(filename, data)
		if not Utils.canAccessFS then 
			return
		end
		
		if not io then
			Utils.canAccessFS = false
			trigger.action.outText('Persistance disabled', 30)
			return
		end
	
		local str = JSON:encode(data)
		-- local str = 'return (function() local tbl = {}'
		-- for i,v in pairs(data) do
		-- 	str = str..'\ntbl[\''..i..'\'] = '..Utils.serializeValue(v)
		-- end
		
		-- str = str..'\nreturn tbl end)()'
	
		local File = io.open(filename, "w")
		File:write(str)
		File:close()
	end
	
	function Utils.serializeValue(value)
		local res = ''
		if type(value)=='number' or type(value)=='boolean' then
			res = res..tostring(value)
		elseif type(value)=='string' then
			res = res..'\''..value..'\''
		elseif type(value)=='table' then
			res = res..'{ '
			for i,v in pairs(value) do
				if type(i)=='number' then
					res = res..'['..i..']='..Utils.serializeValue(v)..','
				else
					res = res..'[\''..i..'\']='..Utils.serializeValue(v)..','
				end
			end
			res = res:sub(1,-2)
			res = res..' }'
		end
		return res
	end
	
	function Utils.loadTable(filename)
		if not Utils.canAccessFS then 
			return
		end
		
		if not lfs then
			Utils.canAccessFS = false
			trigger.action.outText('Persistance disabled', 30)
			return
		end
		
		if lfs.attributes(filename) then
			local File = io.open(filename, "r")
			local str = File:read('*all')
			File:close()

			return JSON:decode(str)
		end
	end
	
	function Utils.merge(table1, table2)
		local result = {}
		for i,v in pairs(table1) do
			result[i] = v
		end
		
		for i,v in pairs(table2) do
			result[i] = v
		end
		
		return result
	end

	function Utils.log(func)
		return function(p1,p2,p3,p4,p5,p6,p7,p8,p9,p10)
			local err, msg = pcall(func,p1,p2,p3,p4,p5,p6,p7,p8,p9,p10)
			if not err then
				env.info("ERROR - callFunc\n"..msg)
				env.info('Traceback\n'..debug.traceback())
			end
		end
	end

	function Utils.getAmmo(group, type)
		local count = 0
		for _, u in ipairs(group:getUnits()) do
			if u:isExist() then
				local ammo = u:getAmmo()
				for i,v in pairs(ammo) do
					if v.desc.typeName == type then
						count = count + v.count
					end
				end
			end
		end

		return count
	end
end



-----------------[[ END OF Utils.lua ]]-----------------



-----------------[[ MenuRegistry.lua ]]-----------------

MenuRegistry = {}

do
    MenuRegistry.menus = {}
    function MenuRegistry:register(order, registerfunction, context)
        for i=1,order,1 do
            if not MenuRegistry.menus[i] then MenuRegistry.menus[i] = {func = function() end, context = {}} end
        end

        MenuRegistry.menus[order] = {func = registerfunction, context = context}
    end

    local ev = {}
    function ev:onEvent(event)
        if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
            local player = event.initiator:getPlayerName()
            if player then
                env.info('MenuRegistry - creating menus for player: '..player)
                for i,v in ipairs(MenuRegistry.menus) do
                    local err, msg = pcall(v.func, event, v.context)
                    if not err then
                        env.info("MenuRegistry - ERROR :\n"..msg)
                        env.info('Traceback\n'..debug.traceback())
                    end
                end
            end
        end
    end
    
    world.addEventHandler(ev)

    function MenuRegistry.showTargetZoneMenu(groupid, name, action, targetside, minDistToFront, data, includeCarriers, onlyRevealed)
		local zones = ZoneCommand.getAllZones()

        if targetside and type(targetside) == 'number' then
            targetside = { targetside }
        end

        local zns = {}
        for i,v in pairs(zones) do
            if not targetside or Utils.isInArray(v.side,targetside) then 
                if not minDistToFront or v.distToFront <= minDistToFront then
                    if not onlyRevealed or v.revealTime>0 then
                        table.insert(zns, v)
                    end
                end
            end
        end

        if includeCarriers then
            for i,v in pairs(CarrierCommand.getAllCarriers()) do
                if not targetside or Utils.isInArray(v.side,targetside) then 
                    table.insert(zns, v)
                end
            end
        end

        if #zns == 0 then return false end

        table.sort(zns, function(a,b) return a.name < b.name end)

        local executeAction = function(act, params)
			local err = act(params)
			if not err then
				missionCommands.removeItemForGroup(params.groupid, params.menu)
			end
		end

		local menu = missionCommands.addSubMenuForGroup(groupid, name)
		local sub1 = nil

		local count = 0
		for i,v in ipairs(zns) do
            count = count + 1
            if count<10 then
                missionCommands.addCommandForGroup(groupid, v.name, menu, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data})
            elseif count==10 then
                sub1 = missionCommands.addSubMenuForGroup(groupid, "More", menu)
                missionCommands.addCommandForGroup(groupid, v.name, sub1, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data})
            elseif count%9==1 then
                sub1 = missionCommands.addSubMenuForGroup(groupid, "More", sub1)
                missionCommands.addCommandForGroup(groupid, v.name, sub1, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data})
            else
                missionCommands.addCommandForGroup(groupid, v.name, sub1, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data})
            end
		end
		
		return menu
    end
end

-----------------[[ END OF MenuRegistry.lua ]]-----------------



-----------------[[ CustomZone.lua ]]-----------------


CustomZone = {}
do
	function CustomZone:getByName(name)
		local obj = {}
		obj.name = name
		
		local zd = nil
		for _,v in ipairs(env.mission.triggers.zones) do
			if v.name == name then
				zd = v
				break
			end
		end
		
		if not zd then
			return nil
		end
		
		obj.type = zd.type -- 2 == quad, 0 == circle
		if obj.type == 2 then
			obj.vertices = {}
			for _,v in ipairs(zd.verticies) do
				local vertex = {
					x = v.x,
					y = 0,
					z = v.y
				}
				table.insert(obj.vertices, vertex)
			end
		end
		
		obj.radius = zd.radius
		obj.point = {
			x = zd.x,
			y = 0,
			z = zd.y
		}
		
		setmetatable(obj, self)
		self.__index = self
		return obj
	end
	
	function CustomZone:isQuad()
		return self.type==2
	end
	
	function CustomZone:isCircle()
		return self.type==0
	end
	
	function CustomZone:isInside(point)
		if self:isCircle() then
			local dist = mist.utils.get2DDist(point, self.point)
			return dist<self.radius
		elseif self:isQuad() then
			return mist.pointInPolygon(point, self.vertices)
		end
	end
	
	function CustomZone:draw(id, border, background)
		if self:isCircle() then
			trigger.action.circleToAll(-1,id,self.point, self.radius,border,background,1)
		elseif self:isQuad() then
			trigger.action.quadToAll(-1,id,self.vertices[4], self.vertices[3], self.vertices[2], self.vertices[1],border,background,1)
		end
	end
	
	function CustomZone:getRandomSpawnZone()
		local spawnZones = {}
		for i=1,100,1 do
			local zname = self.name..'-'..i
			if trigger.misc.getZone(zname) then
				table.insert(spawnZones, zname)
			else
				break
			end
		end
		
		if #spawnZones == 0 then return nil end
		
		local choice = math.random(1, #spawnZones)
		return spawnZones[choice]
	end
	
	function CustomZone:spawnGroup(product, acceptedSurface)
		local spname = self.name
		local spawnzone = nil
		
		if not spawnzone then
			spawnzone = self:getRandomSpawnZone()
		end
		
		if spawnzone then
			spname = spawnzone
		end

		if not acceptedSurface then
			acceptedSurface = {
				[land.SurfaceType.LAND] = true
			}
		end
		
		local pnt = mist.getRandomPointInZone(spname)
		for i=1,500,1 do
			if acceptedSurface[land.getSurfaceType(pnt)] then
				break
			end

			pnt = mist.getRandomPointInZone(spname)
		end

		local newgr = Spawner.createObject(product.name, product.template, pnt, product.side, nil, nil, acceptedSurface, spname)

		return newgr
	end
end






-----------------[[ END OF CustomZone.lua ]]-----------------



-----------------[[ PlayerLogistics.lua ]]-----------------

PlayerLogistics = {}
do
	PlayerLogistics.allowedTypes = {}
	PlayerLogistics.allowedTypes['Mi-24P'] = { supplies = true, personCapacity = 8 }
	PlayerLogistics.allowedTypes['Mi-8MT'] = { supplies = true, personCapacity = 24 }
	PlayerLogistics.allowedTypes['UH-1H'] = { supplies = true, personCapacity = 12}
	PlayerLogistics.allowedTypes['Hercules'] = { supplies = true, personCapacity = 92 }
	PlayerLogistics.allowedTypes['UH-60L'] = { supplies = true, personCapacity = 12 }
	PlayerLogistics.allowedTypes['Ka-50'] = { supplies = false }
	PlayerLogistics.allowedTypes['Ka-50_3'] = { supplies = false }
	PlayerLogistics.allowedTypes['SA342L'] = { supplies = false, personCapacity = 2}
	PlayerLogistics.allowedTypes['SA342M'] = { supplies = false, personCapacity = 2}
	PlayerLogistics.allowedTypes['SA342Minigun'] = { supplies = false, personCapacity = 2}
	PlayerLogistics.allowedTypes['AH-64D_BLK_II'] = { supplies = false }

	PlayerLogistics.infantryTypes = {
		capture = 'capture',
		sabotage = 'sabotage',
		ambush = 'ambush',
		engineer = 'engineer',
		manpads = 'manpads',
		spy = 'spy',
		rapier = 'rapier',
		extractable = 'extractable'
	}

	function PlayerLogistics.getInfantryName(infType)
		if infType==PlayerLogistics.infantryTypes.capture then
			return "Capture Squad"
		elseif infType==PlayerLogistics.infantryTypes.sabotage then
			return "Sabotage Squad"
		elseif infType==PlayerLogistics.infantryTypes.ambush then
			return "Ambush Squad"
		elseif infType==PlayerLogistics.infantryTypes.engineer then
			return "Engineer"
		elseif infType==PlayerLogistics.infantryTypes.manpads then
			return "MANPADS"
		elseif infType==PlayerLogistics.infantryTypes.spy then
			return "Spy"
		elseif infType==PlayerLogistics.infantryTypes.rapier then
			return "Rapier SAM"
		elseif infType==PlayerLogistics.infantryTypes.extractable then
			return "Extracted infantry"
		end

		return "INVALID SQUAD"
	end
	
	function PlayerLogistics:new()
		local obj = {}
		obj.groupMenus = {} -- groupid = path
		obj.carriedCargo = {} -- groupid = source
		obj.carriedInfantry = {} -- groupid = source
		obj.carriedPilots = {} --groupid = source
		obj.registeredSquadGroups = {}
		obj.lastLoaded = {} -- groupid = zonename

		obj.hercTracker = {
			cargos = {},
			cargoCheckFunctions = {}
		}

		obj.hercPreparedDrops = {}
		
		setmetatable(obj, self)
		self.__index = self
		
		obj:start()
		
		DependencyManager.register("PlayerLogistics", obj)
		return obj
	end

	function PlayerLogistics:registerSquadGroup(squadType, groupname, weight, cost, jobtime, extracttime, squadSize, side)
		self.registeredSquadGroups[squadType] = { name=groupname, type=squadType, weight=weight, cost=cost, jobtime=jobtime, extracttime=extracttime, size = squadSize, side=side}
	end
	
	function PlayerLogistics:start()
		if not ZoneCommand then return end
	
        MenuRegistry:register(3, function(event, context)
			if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
				local player = event.initiator:getPlayerName()
				if player then
					local unitType = event.initiator:getDesc()['typeName']
					local groupid = event.initiator:getGroup():getID()
					local groupname = event.initiator:getGroup():getName()
					
					local logistics = context.allowedTypes[unitType]
					if logistics and (logistics.supplies or logistics.personCapacity)then

						if context.groupMenus[groupid] then
							missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
							context.groupMenus[groupid] = nil
						end

						if not context.groupMenus[groupid] then
							local size = event.initiator:getGroup():getSize()
							if size > 1 then
								trigger.action.outText('WARNING: group '..groupname..' has '..size..' units. Logistics will only function for group leader', 10)
							end
							
							local cargomenu = missionCommands.addSubMenuForGroup(groupid, 'Logistics')
							if logistics.supplies then
								local supplyMenu = missionCommands.addSubMenuForGroup(groupid, 'Supplies', cargomenu)
								local loadMenu = missionCommands.addSubMenuForGroup(groupid, 'Load', supplyMenu)
								missionCommands.addCommandForGroup(groupid, 'Load 100 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=100})
								missionCommands.addCommandForGroup(groupid, 'Load 500 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=500})
								missionCommands.addCommandForGroup(groupid, 'Load 1000 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=1000})
								missionCommands.addCommandForGroup(groupid, 'Load 2000 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=2000})
								missionCommands.addCommandForGroup(groupid, 'Load 5000 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=5000})

								local unloadMenu = missionCommands.addSubMenuForGroup(groupid, 'Unload', supplyMenu)
								missionCommands.addCommandForGroup(groupid, 'Unload 100 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=100})
								missionCommands.addCommandForGroup(groupid, 'Unload 500 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=500})
								missionCommands.addCommandForGroup(groupid, 'Unload 1000 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=1000})
								missionCommands.addCommandForGroup(groupid, 'Unload 2000 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=2000})
								missionCommands.addCommandForGroup(groupid, 'Unload 5000 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=5000})
								missionCommands.addCommandForGroup(groupid, 'Unload all supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=9999999})
							end

							local sqs = {}
							for sqType,_ in pairs(context.registeredSquadGroups) do
								table.insert(sqs,sqType)
							end
							table.sort(sqs)

							if logistics.personCapacity then
								local infMenu = missionCommands.addSubMenuForGroup(groupid, 'Infantry', cargomenu)
								
								local loadInfMenu = missionCommands.addSubMenuForGroup(groupid, 'Load', infMenu)
								for _,sqType in ipairs(sqs) do
									local menuName =  'Load '..PlayerLogistics.getInfantryName(sqType)
									missionCommands.addCommandForGroup(groupid, menuName, loadInfMenu, Utils.log(context.loadInfantry), context, {group=groupname, type=sqType})
								end

								local unloadInfMenu = missionCommands.addSubMenuForGroup(groupid, 'Unload', infMenu)
								for _,sqType in ipairs(sqs) do
									local menuName =  'Unload '..PlayerLogistics.getInfantryName(sqType)
									missionCommands.addCommandForGroup(groupid, menuName, unloadInfMenu, Utils.log(context.unloadInfantry), context, {group=groupname, type=sqType})
								end
								missionCommands.addCommandForGroup(groupid, 'Unload Extracted squad', unloadInfMenu, Utils.log(context.unloadInfantry), context, {group=groupname, type=PlayerLogistics.infantryTypes.extractable})

								missionCommands.addCommandForGroup(groupid, 'Extract squad', infMenu, Utils.log(context.extractSquad), context, groupname)
								missionCommands.addCommandForGroup(groupid, 'Unload all', infMenu, Utils.log(context.unloadInfantry), context, {group=groupname})

								local csarMenu = missionCommands.addSubMenuForGroup(groupid, 'CSAR', cargomenu)
								missionCommands.addCommandForGroup(groupid, 'Show info (closest)', csarMenu, Utils.log(context.showPilot), context, groupname)
								missionCommands.addCommandForGroup(groupid, 'Smoke marker (closest)', csarMenu, Utils.log(context.smokePilot), context, groupname)
								missionCommands.addCommandForGroup(groupid, 'Flare (closest)', csarMenu, Utils.log(context.flarePilot), context, groupname)
								missionCommands.addCommandForGroup(groupid, 'Extract pilot', csarMenu, Utils.log(context.extractPilot), context, groupname)
								missionCommands.addCommandForGroup(groupid, 'Unload pilots', csarMenu, Utils.log(context.unloadPilots), context, groupname)
							end

							missionCommands.addCommandForGroup(groupid, 'Cargo status', cargomenu, Utils.log(context.cargoStatus), context, groupname)
							missionCommands.addCommandForGroup(groupid, 'Unload Everything', cargomenu, Utils.log(context.unloadAll), context, groupname)
							
							if unitType == 'Hercules' then
								local loadmasterMenu = missionCommands.addSubMenuForGroup(groupid, 'Loadmaster', cargomenu)

								for _,sqType in ipairs(sqs) do
									local menuName =  'Prepare '..PlayerLogistics.getInfantryName(sqType)
									missionCommands.addCommandForGroup(groupid, menuName, loadmasterMenu, Utils.log(context.hercPrepareDrop), context, {group=groupname, type=sqType})
								end

								missionCommands.addCommandForGroup(groupid, 'Prepare Supplies', loadmasterMenu, Utils.log(context.hercPrepareDrop), context, {group=groupname, type='supplies'})
							end
							
							
							context.groupMenus[groupid] = cargomenu
						end
						
						if context.carriedCargo[groupid] then
							context.carriedCargo[groupid] = 0
						end

						if context.carriedInfantry[groupid] then
							context.carriedInfantry[groupid] = {}
						end

						if context.carriedPilots[groupid] then
							context.carriedPilots[groupid] = {}
						end

						if context.lastLoaded[groupid] then
							context.lastLoaded[groupid] = nil
						end

						if context.hercPreparedDrops[groupid] then
							context.hercPreparedDrops[groupid] = nil
						end
					end
				end
			elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
					
					if context.groupMenus[groupid] then
						missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
						context.groupMenus[groupid] = nil
					end
				end
            end
		end, self)

		local ev = {}
		ev.context = self
		function ev:onEvent(event)
			if event.id == world.event.S_EVENT_SHOT and event.initiator and event.initiator:isExist() then
				local unitName = event.initiator:getName()
				local groupId = event.initiator:getGroup():getID()
				local name = event.weapon:getDesc().typeName
				if name == 'weapons.bombs.Generic Crate [20000lb]' then
					local prepared = self.context.hercPreparedDrops[groupId]

					if not prepared then
						prepared = 'supplies'
								
						if self.context.carriedInfantry[groupId] then
							for _,v in ipairs(self.context.carriedInfantry[groupId]) do
								if v.type ~= PlayerLogistics.infantryTypes.extractable then
									prepared = v.type
									break
								end
							end
						end

						env.info('PlayerLogistics - Hercules - auto preparing '..prepared)
					end

					if prepared then
						if prepared == 'supplies' then
							env.info('PlayerLogistics - Hercules - supplies getting dropped')
							local carried = self.context.carriedCargo[groupId]
							local amount = 0
							if carried and carried > 0 then
								amount = 9000
								if carried < amount then
									amount = carried
								end
							end
							
							if amount > 0 then
								self.context.carriedCargo[groupId] = math.max(0,self.context.carriedCargo[groupId] - amount)
								if not self.context.hercTracker.cargos[unitName] then
									self.context.hercTracker.cargos[unitName] = {}
								end
								
								table.insert(self.context.hercTracker.cargos[unitName],{
									object = event.weapon,
									supply = amount,
									lastLoaded = self.context.lastLoaded[groupId],
									unit = event.initiator
								})
								
								env.info('PlayerLogistics - Hercules - '..unitName..' deployed crate with '..amount..' supplies')
								self.context:processHercCargos(unitName)
								self.context.hercPreparedDrops[groupId] = nil
								trigger.action.outTextForUnit(event.initiator:getID(), 'Crate with '..amount..' supplies deployed', 10)
							else
								trigger.action.outTextForUnit(event.initiator:getID(), 'Empty crate deployed', 10)
							end
						else
							env.info('PlayerLogistics - Hercules - searching for prepared infantry')
							local toDrop = nil
							local remaining = {}
							if self.context.carriedInfantry[groupId] then
								for _,v in ipairs(self.context.carriedInfantry[groupId]) do
									if v.type == prepared and toDrop == nil then
										toDrop = v
									else
										table.insert(remaining, v)
									end
								end
							end
							
							
							if toDrop then
								env.info('PlayerLogistics - Hercules - dropping '..toDrop.type)
								if not self.context.hercTracker.cargos[unitName] then
									self.context.hercTracker.cargos[unitName] = {}
								end
								
								table.insert(self.context.hercTracker.cargos[unitName],{
									object = event.weapon,
									squad = toDrop,
									lastLoaded = self.context.lastLoaded[groupId],
									unit = event.initiator
								})
								
								env.info('PlayerLogistics - Hercules - '..unitName..' deployed crate with '..toDrop.type)
								self.context:processHercCargos(unitName)
								self.context.hercPreparedDrops[groupId] = nil
								
								local squadName = PlayerLogistics.getInfantryName(prepared)
								trigger.action.outTextForUnit(event.initiator:getID(), squadName..' crate deployed.', 10)
								self.context.carriedInfantry[groupId] = remaining
								local weight = self.context:getCarriedPersonWeight(event.initiator:getGroup():getName())
								trigger.action.setUnitInternalCargo(event.initiator:getName(), weight)
							else
								trigger.action.outTextForUnit(event.initiator:getID(), 'Empty crate deployed', 10)
							end
						end
					else
						trigger.action.outTextForUnit(event.initiator:getID(), 'Empty crate deployed', 10)
					end
				end
			end
		end
		
		world.addEventHandler(ev)
	end

	function PlayerLogistics:processHercCargos(unitName)
		if not self.hercTracker.cargoCheckFunctions[unitName] then
			env.info('PlayerLogistics - Hercules - start tracking cargos of '..unitName)
			self.hercTracker.cargoCheckFunctions[unitName] = timer.scheduleFunction(function(params, time)
				local reschedule = params.context:checkHercCargo(params.unitName, time)
				if not reschedule then
					params.context.hercTracker.cargoCheckFunctions[params.unitName] = nil
					env.info('PlayerLogistics - Hercules - stopped tracking cargos of '..unitName)
				end
				
				return reschedule
			end, {unitName=unitName, context = self}, timer.getTime() + 0.1)
		end
	end

	function PlayerLogistics:checkHercCargo(unitName, time)
		local cargos = self.hercTracker.cargos[unitName]
		if cargos and #cargos > 0 then
			local remaining = {}
			for _,cargo in ipairs(cargos) do
				if cargo.object and cargo.object:isExist() then
					local alt = Utils.getAGL(cargo.object)
					if alt < 5 then
						self:deliverHercCargo(cargo)
					else
						table.insert(remaining, cargo)
					end
				else
					env.info('PlayerLogistics - Hercules - cargo crashed '..tostring(cargo.supply)..' '..tostring(cargo.squad))
					if cargo.squad then 
						env.info('PlayerLogistics - Hercules - squad crashed '..tostring(cargo.squad.type))
					end

					if cargo.unit and cargo.unit:isExist() then
						if cargo.squad then
							local squadName = PlayerLogistics.getInfantryName(cargo.squad.type)
							trigger.action.outTextForUnit(cargo.unit:getID(), 'Cargo drop of '..cargo.unit:getPlayerName()..' with '..squadName..' crashed', 10)
						elseif cargo.supply then
							trigger.action.outTextForUnit(cargo.unit:getID(), 'Cargo drop of '..cargo.unit:getPlayerName()..' with '..cargo.supply..' supplies crashed', 10)
						end
					end
				end
			end

			if #remaining > 0 then
				self.hercTracker.cargos[unitName] = remaining
				return time + 0.1
			end
		end
	end

	function PlayerLogistics:deliverHercCargo(cargo)
		if cargo.object and cargo.object:isExist() then
			if cargo.supply then
				local zone = ZoneCommand.getZoneOfWeapon(cargo.object)
				if zone then
					zone:addResource(cargo.supply)
					env.info('PlayerLogistics - Hercules - '..cargo.supply..' delivered to '..zone.name)

					self:awardSupplyXP(cargo.lastLoaded, zone, cargo.unit, cargo.supply)
				end
			elseif cargo.squad then
				local pos = Utils.getPointOnSurface(cargo.object:getPoint())
				pos.y = pos.z
				pos.z = nil
				local surface = land.getSurfaceType(pos)
				if surface == land.SurfaceType.LAND or surface == land.SurfaceType.ROAD or surface == land.SurfaceType.RUNWAY then
					local zn = ZoneCommand.getZoneOfPoint(pos)
					
					local lastLoad = cargo.squad.loadedAt
					if lastLoad and zn and zn.side == cargo.object:getCoalition() and zn.name==lastLoad.name then
						if self.registeredSquadGroups[cargo.squad.type] then
							local cost = self.registeredSquadGroups[cargo.squad.type].cost
							zn:addResource(cost)
							zn:refreshText()
							if cargo.unit and cargo.unit:isExist() then
								local squadName = PlayerLogistics.getInfantryName(cargo.squad.type)
								trigger.action.outTextForUnit(cargo.unit:getID(), squadName..' unloaded', 10)
							end
						end
					else
						local error = DependencyManager.get("SquadTracker"):spawnInfantry(self.registeredSquadGroups[cargo.squad.type], pos)
						if not error then
							env.info('PlayerLogistics - Hercules - '..cargo.squad.type..' deployed')
							
							local squadName = PlayerLogistics.getInfantryName(cargo.squad.type)
							
							if cargo.unit and cargo.unit:isExist() and cargo.unit.getPlayerName then
								trigger.action.outTextForUnit(cargo.unit:getID(), squadName..' deployed', 10)
								local player = cargo.unit:getPlayerName()
								local xp = RewardDefinitions.actions.squadDeploy * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player)
								
								DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp)
								
								if zn then
									DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, zn.name, cargo.squad.type)
								else
									DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, '', cargo.squad.type)
								end
								trigger.action.outTextForUnit(cargo.unit:getID(), '+'..math.floor(xp)..' XP', 10)
							end
						end
					end
				else
					env.info('PlayerLogistics - Hercules - '..cargo.squad.type..' dropped on invalid surface '..tostring(surface))
					local cpos = cargo.object:getPoint()
					env.info('PlayerLogistics - Hercules - cargo spot X:'..cpos.x..' Y:'..cpos.y..' Z:'..cpos.z)
					env.info('PlayerLogistics - Hercules - surface spot X:'..pos.x..' Y:'..pos.y)
					local squadName = PlayerLogistics.getInfantryName(cargo.squad.type)
					trigger.action.outTextForUnit(cargo.unit:getID(), 'Cargo drop of '..cargo.unit:getPlayerName()..' with '..squadName..' crashed', 10)
				end
			end

			cargo.object:destroy()
		end
	end

	function PlayerLogistics:hercPrepareDrop(params)
		local groupname = params.group
		local type = params.type
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)

			if type == 'supplies' then
				local cargo = self.carriedCargo[gr:getID()]
				if cargo and cargo > 0 then
					self.hercPreparedDrops[gr:getID()] = type
					trigger.action.outTextForUnit(un:getID(), 'Supply drop prepared', 10)
				else
					trigger.action.outTextForUnit(un:getID(), 'No supplies onboard the aircraft', 10)
				end
			else
				local exists = false
				if self.carriedInfantry[gr:getID()] then
					for i,v in ipairs(self.carriedInfantry[gr:getID()]) do
						if v.type == type then
							exists = true
							break
						end
					end
				end
			
				if exists then
					self.hercPreparedDrops[gr:getID()] = type
					local squadName = PlayerLogistics.getInfantryName(type)
					trigger.action.outTextForUnit(un:getID(), squadName..' drop prepared', 10)
				else
					local squadName = PlayerLogistics.getInfantryName(type)
					trigger.action.outTextForUnit(un:getID(), 'No '..squadName..' onboard the aircraft', 10)
				end
			end
		end
	end

	function PlayerLogistics:awardSupplyXP(lastLoad, zone, unit, amount)
		if lastLoad and zone.name~=lastLoad.name and not zone.isCarrier and not lastLoad.isCarrier then
			if unit and unit.isExist and unit:isExist() and unit.getPlayerName then
				local player = unit:getPlayerName()
				local xp = amount*RewardDefinitions.actions.supplyRatio

				local totalboost = 0
				local dist = mist.utils.get2DDist(lastLoad.zone.point, zone.zone.point)
				if dist > 15000 then
					local extradist = math.max(dist - 15000, 85000)
					local kmboost = extradist/85000
					local actualboost = xp * kmboost * 1
					totalboost = totalboost + actualboost
				end

				local both = true
				if zone:criticalOnSupplies() then
					local actualboost = xp * RewardDefinitions.actions.supplyBoost
					totalboost = totalboost + actualboost
				else 
					both = false
				end

				if zone.distToFront == 0 then
					local actualboost = xp * RewardDefinitions.actions.supplyBoost
					totalboost = totalboost + actualboost
				else 
					both = false
				end

				if both then
					local actualboost = xp * 1
					totalboost = totalboost + actualboost
				end

				xp = xp + totalboost

				if lastLoad.distToFront >= zone.distToFront then
					xp = xp * 0.25
				end

				xp = xp * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player)

				DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp)
				DependencyManager.get("MissionTracker"):tallySupplies(player, amount, zone.name)
				trigger.action.outTextForUnit(unit:getID(), '+'..math.floor(xp)..' XP', 10)
			end
		end
	end
	
	function PlayerLogistics.markWithSmoke(zonename)
		local zone = CustomZone:getByName(zonename)
		local p = Utils.getPointOnSurface(zone.point)
		trigger.action.smoke(p, 0)
	end
	
	function PlayerLogistics.getWeight(supplies)
		return math.floor(supplies)
	end

	function PlayerLogistics:getCarriedPersonWeight(groupname)
		local gr = Group.getByName(groupname)
		local un = gr:getUnit(1)
		if un then
			if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return 0 end
			
			local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity

			local pilotWeight = 0
			local squadWeight = 0
			if not self.carriedPilots[gr:getID()] then self.carriedPilots[gr:getID()] = {} end
			local pilots = self.carriedPilots[gr:getID()]
			if pilots then 
				pilotWeight = 100 * #pilots
			end

			if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end
			local squads = self.carriedInfantry[gr:getID()]
			if squads then
				for _,squad in ipairs(squads) do 
					squadWeight = squadWeight + squad.weight
				end
			end

			return pilotWeight + squadWeight
		end
	end

	function PlayerLogistics:getOccupiedPersonCapacity(groupname)
		local gr = Group.getByName(groupname)
		local un = gr:getUnit(1)
		if un then
			if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return 0 end
			if self.carriedCargo[gr:getID()] and self.carriedCargo[gr:getID()] > 0 then return 0 end
			
			local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity

			local pilotCount = 0
			local squadCount = 0
			if not self.carriedPilots[gr:getID()] then self.carriedPilots[gr:getID()] = {} end
			local pilots = self.carriedPilots[gr:getID()]
			if pilots then 
				pilotCount = #pilots
			end

			if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end
			local squads = self.carriedInfantry[gr:getID()]
			if squads then
				for _,squad in ipairs(squads) do 
					squadCount = squadCount + squad.size
				end
			end

			local total = pilotCount + squadCount

			return total
		end
	end

	function PlayerLogistics:getRemainingPersonCapacity(groupname)
		local gr = Group.getByName(groupname)
		local un = gr:getUnit(1)
		if un then
			if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return 0 end
			if self.carriedCargo[gr:getID()] and self.carriedCargo[gr:getID()] > 0 then return 0 end
			
			local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity

			local total = self:getOccupiedPersonCapacity(groupname)

			return max - total
		end
	end

	function PlayerLogistics:canFitCargo(groupname)
		local gr = Group.getByName(groupname)
		local un = gr:getUnit(1)
		if un then
			if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return false end
			return self:getOccupiedPersonCapacity(groupname) == 0
		end
	end

	function PlayerLogistics:canFitPersonnel(groupname, toFit)
		local gr = Group.getByName(groupname)
		local un = gr:getUnit(1)
		if un then
			if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return false end

			return self:getRemainingPersonCapacity(groupname) >= toFit
		end
	end

	function PlayerLogistics:showPilot(groupname)
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un then
				local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint())
				
				if not data then 
					trigger.action.outTextForUnit(un:getID(), 'No pilots in need of extraction', 10)
					return
				end

				local pos = data.pilot:getUnit(1):getPoint()
				local brg = math.floor(Utils.getBearing(un:getPoint(), data.pilot:getUnit(1):getPoint()))
				local dist = data.dist
				local dstft = math.floor(dist/0.3048)

				local msg = data.name..' requesting extraction'
				msg = msg..'\n\n Distance: '
				if dist>1000 then
					local dstkm = string.format('%.2f',dist/1000)
					local dstnm = string.format('%.2f',dist/1852)
					
					msg = msg..dstkm..'km | '..dstnm..'nm'
				else
					local dstft = math.floor(dist/0.3048)
					msg = msg..math.floor(dist)..'m | '..dstft..'ft'
				end
				
				msg = msg..'\n Bearing: '..brg
				
				trigger.action.outTextForUnit(un:getID(), msg, 10)
			end
		end
	end
	
	function PlayerLogistics:smokePilot(groupname)
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un then
				local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint())
				
				if not data or data.dist >= 5000 then 
					trigger.action.outTextForUnit(un:getID(), 'No pilots nearby', 10)
					return
				end

				DependencyManager.get("CSARTracker"):markPilot(data)
				trigger.action.outTextForUnit(un:getID(), 'Location of '..data.name..' marked with green smoke.', 10)
			end
		end
	end
	
	function PlayerLogistics:flarePilot(groupname)
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un then
				local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint())
				
				if not data or data.dist >= 5000 then 
					trigger.action.outTextForUnit(un:getID(), 'No pilots nearby', 10)
					return
				end

				DependencyManager.get("CSARTracker"):flarePilot(data)
				trigger.action.outTextForUnit(un:getID(), data.name..' has deployed a green flare', 10)
			end
		end
	end
	
	function PlayerLogistics:unloadPilots(groupname)
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un then
				local pilots = self.carriedPilots[gr:getID()]
				if not pilots or #pilots==0 then
					trigger.action.outTextForUnit(un:getID(), 'No pilots onboard', 10)
					return
				end

				if Utils.isInAir(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not unload pilot while in air', 10)
					return
				end
				
				if not self:isCargoDoorOpen(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not unload pilot while cargo door closed', 10)
					return
				end

				local zn = ZoneCommand.getZoneOfUnit(un:getName())
				if not zn then 
					zn = CarrierCommand.getCarrierOfUnit(un:getName())
				end
				
				if not zn then
					trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted pilots while within a friendly zone', 10)
					return
				end
				
				if zn.side ~= 0 and zn.side ~= un:getCoalition()then
					trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted pilots while within a friendly zone', 10)
					return
				end

				zn:addResource(200*#pilots)
				zn:refreshText()

				if un.getPlayerName then
					local player = un:getPlayerName()

					local xp = #pilots*RewardDefinitions.actions.pilotExtract

					xp = xp * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player)

					DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp)
					DependencyManager.get("MissionTracker"):tallyUnloadPilot(player, zn.name)
					trigger.action.outTextForUnit(un:getID(), '+'..math.floor(xp)..' XP', 10)
				end

				self.carriedPilots[gr:getID()] = {}
				trigger.action.setUnitInternalCargo(un:getName(), 0)
				trigger.action.outTextForUnit(un:getID(), 'Pilots unloaded', 10)
			end
		end
	end
	
	function PlayerLogistics:extractPilot(groupname)
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un then
				if not self:canFitPersonnel(groupname, 1) then
					trigger.action.outTextForUnit(un:getID(), 'Not enough free space onboard. (Need 1)', 10)
					return
				end

				timer.scheduleFunction(function(param,time) 
					local self = param.context
					local un = param.unit
					if not un then return end
					if not un:isExist() then return end
					local gr = un:getGroup()

					local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint())

					if not data or data.dist > 500 then
						trigger.action.outTextForUnit(un:getID(), 'There is no pilot nearby that needs extraction', 10)
						return
					else
						if not self:isCargoDoorOpen(un) then
							trigger.action.outTextForUnit(un:getID(), 'Cargo door closed', 1)
						elseif Utils.getAGL(un) > 70 then
							trigger.action.outTextForUnit(un:getID(), 'Altitude too high (< 70 m). Current: '..string.format('%.2f',Utils.getAGL(un))..' m', 1)
						elseif mist.vec.mag(un:getVelocity())>5 then
							trigger.action.outTextForUnit(un:getID(), 'Moving too fast (< 5 m/s). Current: '..string.format('%.2f',mist.vec.mag(un:getVelocity()))..' m/s', 1)
						else
							if data.dist > 100 then
								trigger.action.outTextForUnit(un:getID(), 'Too far (< 100m). Current: '..string.format('%.2f',data.dist)..' m', 1)
							else
								if not self.carriedPilots[gr:getID()] then self.carriedPilots[gr:getID()] = {} end
								table.insert(self.carriedPilots[gr:getID()], data.name)
								local player = un:getPlayerName()
								DependencyManager.get("MissionTracker"):tallyLoadPilot(player, data)
								DependencyManager.get("CSARTracker"):removePilot(data.name)
								local weight = self:getCarriedPersonWeight(gr:getName())
								trigger.action.setUnitInternalCargo(un:getName(), weight)
								trigger.action.outTextForUnit(un:getID(), data.name..' onboard. ('..weight..' kg)', 10)
								return
							end
						end
					end

					param.trys = param.trys - 1
					if param.trys > 0 then
						return time+1
					end
				end, {context = self, unit = un, trys = 60}, timer.getTime()+0.1)
			end
		end
	end
	
	function PlayerLogistics:extractSquad(groupname)
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un then

				if Utils.isInAir(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while in air', 10)
					return
				end
				
				if not self:isCargoDoorOpen(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while cargo door closed', 10)
					return
				end

				local squad, distance = DependencyManager.get("SquadTracker"):getClosestExtractableSquad(un:getPoint(), un:getCoalition())
				if squad and distance < 50 then
					local squadgr = Group.getByName(squad.name)
					if squadgr and squadgr:isExist() then
						local sqsize = squadgr:getSize()
						if not self:canFitPersonnel(groupname, sqsize) then
							trigger.action.outTextForUnit(un:getID(), 'Not enough free space onboard. (Need '..sqsize..')', 10)
							return
						end

						if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end
						table.insert(self.carriedInfantry[gr:getID()],{type = PlayerLogistics.infantryTypes.extractable, size = sqsize, weight = sqsize * 100})
						
						local weight = self:getCarriedPersonWeight(gr:getName())

						trigger.action.setUnitInternalCargo(un:getName(), weight)
						
						local loadedInfName = PlayerLogistics.getInfantryName(PlayerLogistics.infantryTypes.extractable)
						trigger.action.outTextForUnit(un:getID(), loadedInfName..' onboard. ('..weight..' kg)', 10)
						
						local player = un:getPlayerName()
						DependencyManager.get("MissionTracker"):tallyLoadSquad(player, squad)
						DependencyManager.get("SquadTracker"):removeSquad(squad.name)
						
						squadgr:destroy()
					end
				else
					trigger.action.outTextForUnit(un:getID(), 'There is no infantry nearby that is ready to be extracted.', 10)
				end
			end
		end
	end

	function PlayerLogistics:loadInfantry(params)
		if not ZoneCommand then return end
		
		local gr = Group.getByName(params.group)
		local squadType = params.type
		local squadName = PlayerLogistics.getInfantryName(squadType)
		
		local squadCost = 0
		local squadSize = 999999
		local squadWeight = 0
		if self.registeredSquadGroups[squadType] then
			squadCost = self.registeredSquadGroups[squadType].cost
			squadSize = self.registeredSquadGroups[squadType].size
			squadWeight = self.registeredSquadGroups[squadType].weight
		end
		
		if gr then
			local un = gr:getUnit(1)
			if un then
				if Utils.isInAir(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while in air', 10)
					return
				end
				
				local zn = ZoneCommand.getZoneOfUnit(un:getName())
				if not zn then 
					zn = CarrierCommand.getCarrierOfUnit(un:getName())
				end
				
				if not zn then
					trigger.action.outTextForUnit(un:getID(), 'Can only load infantry while within a friendly zone', 10)
					return
				end
				
				if zn.side ~= un:getCoalition() then
					trigger.action.outTextForUnit(un:getID(), 'Can only load infantry while within a friendly zone', 10)
					return
				end

				if not self:isCargoDoorOpen(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while cargo door closed', 10)
					return
				end

				if zn:criticalOnSupplies() then
					trigger.action.outTextForUnit(un:getID(), 'Can not load infantry, zone is low on resources', 10)
					return
				end

				if zn.resource < zn.spendTreshold + squadCost then
					trigger.action.outTextForUnit(un:getID(), 'Can not afford to load '..squadName..' (Cost: '..squadCost..'). Resources would fall to a critical level.', 10)
					return
				end

				if not self:canFitPersonnel(params.group, squadSize) then
					trigger.action.outTextForUnit(un:getID(), 'Not enough free space on board. (Need '..squadSize..')', 10)
					return
				end
				
				zn:removeResource(squadCost)
				zn:refreshText()
				if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end
				table.insert(self.carriedInfantry[gr:getID()],{ type = squadType, size = squadSize, weight = squadWeight, loadedAt = zn })
				self.lastLoaded[gr:getID()] = zn
				
				local weight = self:getCarriedPersonWeight(gr:getName())
				trigger.action.setUnitInternalCargo(un:getName(), weight)
				
				local loadedInfName = PlayerLogistics.getInfantryName(squadType)
				trigger.action.outTextForUnit(un:getID(), loadedInfName..' onboard. ('..weight..' kg)', 10)
			end
		end
	end

	function PlayerLogistics:unloadInfantry(params)
		if not ZoneCommand then return end
		local groupname = params.group
		local sqtype = params.type
		
		local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un then
				if Utils.isInAir(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not unload infantry while in air', 10)
					return
				end

				if not self:isCargoDoorOpen(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not unload infantry while cargo door closed', 10)
					return
				end

				local carriedSquads = self.carriedInfantry[gr:getID()]
				if not carriedSquads or #carriedSquads == 0 then
					trigger.action.outTextForUnit(un:getID(), 'No infantry onboard', 10)
					return
				end
				
				local toUnload = carriedSquads
				local remaining = {}
				if sqtype then
					toUnload = {}
					local sqToUnload = nil
					for _,sq in ipairs(carriedSquads) do
						if sq.type == sqtype and not sqToUnload then
							sqToUnload = sq
						else
							table.insert(remaining, sq)
						end
					end

					if sqToUnload then toUnload = { sqToUnload } end
				end

				if #toUnload == 0 then
					if sqtype then
						local squadName = PlayerLogistics.getInfantryName(sqtype)
						trigger.action.outTextForUnit(un:getID(), 'No '..squadName..' onboard.', 10)
					else
						trigger.action.outTextForUnit(un:getID(), 'No infantry onboard.', 10)
					end

					return
				end
				
				local zn = ZoneCommand.getZoneOfUnit(un:getName())
				if not zn then 
					zn = CarrierCommand.getCarrierOfUnit(un:getName())
				end

				for _, sq in ipairs(toUnload) do
					local squadName = PlayerLogistics.getInfantryName(sq.type)
					local lastLoad = sq.loadedAt
					if lastLoad and zn and zn.side == un:getCoalition() and zn.name==lastLoad.name then
						if self.registeredSquadGroups[sq.type] then
							local cost = self.registeredSquadGroups[sq.type].cost
							zn:addResource(cost)
							zn:refreshText()
							trigger.action.outTextForUnit(un:getID(), squadName..' unloaded', 10)
						end
					else
						if sq.type == PlayerLogistics.infantryTypes.extractable then
							if not zn then
								trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted infantry while within a friendly zone', 10)
								table.insert(remaining, sq)
							elseif zn.side ~= un:getCoalition() then
								trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted infantry while within a friendly zone', 10)
								table.insert(remaining, sq)
							else
								trigger.action.outTextForUnit(un:getID(), 'Infantry recovered', 10)
								zn:addResource(200)
								zn:refreshText()
								
								if un.getPlayerName then
									local player = un:getPlayerName()
									local xp = RewardDefinitions.actions.squadExtract * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player)
			
									DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp)
									DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, zn.name, sq.type)
									trigger.action.outTextForUnit(un:getID(), '+'..math.floor(xp)..' XP', 10)
								end
							end
						elseif self.registeredSquadGroups[sq.type] then
							local pos = Utils.getPointOnSurface(un:getPoint())
		
							local error = DependencyManager.get("SquadTracker"):spawnInfantry(self.registeredSquadGroups[sq.type], pos)
							
							if not error then
								trigger.action.outTextForUnit(un:getID(), squadName..' deployed', 10)
		
								if un.getPlayerName then
									local player = un:getPlayerName()
									local xp = RewardDefinitions.actions.squadDeploy * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player)
				
									DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp)

									if zn then
										DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, zn.name, sq.type)
									else
										DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, '', sq.type)
									end
									trigger.action.outTextForUnit(un:getID(), '+'..math.floor(xp)..' XP', 10)
								end
							else
								trigger.action.outTextForUnit(un:getID(), 'Failed to deploy squad, no suitable location nearby', 10)
								table.insert(remaining, sq)
							end
						else
							trigger.action.outText("ERROR: SQUAD TYPE NOT REGISTERED", 60)
						end
					end
				end
				
				self.carriedInfantry[gr:getID()] = remaining
				local weight = self:getCarriedPersonWeight(groupname)
				trigger.action.setUnitInternalCargo(un:getName(), weight)
			end
		end
	end

	function PlayerLogistics:unloadAll(groupname)
		local gr = Group.getByName(groupname)
		if gr then 
			local un = gr:getUnit(1)
			if un then
				local cargo = self.carriedCargo[gr:getID()]
				local squad = self.carriedInfantry[gr:getID()]
				local pilot = self.carriedPilots[gr:getID()]

				if cargo and cargo>0 then
					self:unloadSupplies({group=groupname, amount=9999999})
				end

				if squad and #squad>0 then
					self:unloadInfantry({group=groupname})
				end

				if pilot and #pilot>0 then
					self:unloadPilots(groupname)
				end
			end
		end
	end
	
	function PlayerLogistics:cargoStatus(groupName)
		local gr = Group.getByName(groupName)
		if gr then
			local un = gr:getUnit(1)
			if un then
				local onboard = self.carriedCargo[gr:getID()]
				if onboard and onboard > 0 then
					local weight = self.getWeight(onboard)
					trigger.action.outTextForUnit(un:getID(), onboard..' supplies onboard. ('..weight..' kg)', 10)
				else
					local msg = ''
					local squads = self.carriedInfantry[gr:getID()]
					if squads and #squads>0 then
						msg = msg..'Squads:\n'

						for _,squad in ipairs(squads) do
							local infName = PlayerLogistics.getInfantryName(squad.type)
							msg = msg..'  \n'..infName..' (Size: '..squad.size..')'
						end
					end

					local pilots = self.carriedPilots[gr:getID()]
					if pilots and #pilots>0 then
						msg = msg.."\n\nPilots:\n"
						for i,v in ipairs(pilots) do
							msg = msg..'\n '..v
						end

					end
					
					local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity
					local occupied = self:getOccupiedPersonCapacity(groupName)

					msg = msg..'\n\nCapacity: '..occupied..'/'..max
					
					msg = msg..'\n('..self:getCarriedPersonWeight(groupName)..' kg)'

					if un:getDesc().typeName == 'Hercules' then
						local preped = self.hercPreparedDrops[gr:getID()]
						if preped then
							if preped == 'supplies' then
								msg = msg..'\nSupplies prepared for next drop.'
							else
								local squadName = PlayerLogistics.getInfantryName(preped)
								msg = msg..'\n'..squadName..' prepared for next drop.'
							end
						end
					end

					trigger.action.outTextForUnit(un:getID(), msg, 10)
				end
			end
		end
	end

	function PlayerLogistics:isCargoDoorOpen(unit)
		if unit then
			local tp = unit:getDesc().typeName
			if tp == "Mi-8MT" then
				if unit:getDrawArgumentValue(86) == 1 then return true end
				if unit:getDrawArgumentValue(38) > 0.85 then return true end
			elseif tp == "UH-1H" then
				if unit:getDrawArgumentValue(43) == 1 then return true end
				if unit:getDrawArgumentValue(44) == 1 then return true end
			elseif tp == "Mi-24P" then
				if unit:getDrawArgumentValue(38) == 1 then return true end
				if unit:getDrawArgumentValue(86) == 1 then return true end
			elseif tp == "Hercules" then
				if unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1 then return true end
			elseif tp == "UH-60L" then
				if unit:getDrawArgumentValue(401) == 1 then return true end
				if unit:getDrawArgumentValue(402) == 1 then return true end
			elseif tp == "SA342Mistral" then
				if unit:getDrawArgumentValue(34) == 1 then return true end
				if unit:getDrawArgumentValue(38) == 1 then return true end
			elseif tp == "SA342L" then
				if unit:getDrawArgumentValue(34) == 1 then return true end
				if unit:getDrawArgumentValue(38) == 1 then return true end
			elseif tp == "SA342M" then
				if unit:getDrawArgumentValue(34) == 1 then return true end
				if unit:getDrawArgumentValue(38) == 1 then return true end
			elseif tp == "SA342Minigun" then
				if unit:getDrawArgumentValue(34) == 1 then return true end
				if unit:getDrawArgumentValue(38) == 1 then return true end
			else
				return true
			end
		end
	end
	
	function PlayerLogistics:loadSupplies(params)
		if not ZoneCommand then return end
		
		local groupName = params.group
		local amount = params.amount
		
		local gr = Group.getByName(groupName)
		if gr then
			local un = gr:getUnit(1)
			if un then
				if not self:canFitCargo(groupName) then
					trigger.action.outTextForUnit(un:getID(), 'Can not load cargo. Personnel onboard.', 10)
					return
				end

				if Utils.isInAir(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not load supplies while in air', 10)
					return
				end
				
				local zn = ZoneCommand.getZoneOfUnit(un:getName())
				if not zn then 
					zn = CarrierCommand.getCarrierOfUnit(un:getName())
				end

				if not zn then
					trigger.action.outTextForUnit(un:getID(), 'Can only load supplies while within a friendly zone', 10)
					return
				end
				
				if zn.side ~= un:getCoalition() then
					trigger.action.outTextForUnit(un:getID(), 'Can only load supplies while within a friendly zone', 10)
					return
				end

				if not self:isCargoDoorOpen(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not load supplies while cargo door closed', 10)
					return
				end

				if zn:criticalOnSupplies() then
					trigger.action.outTextForUnit(un:getID(), 'Can not load supplies, zone is low on resources', 10)
					return
				end

				if zn.resource < zn.spendTreshold + amount then
					trigger.action.outTextForUnit(un:getID(), 'Can not load supplies if resources would fall to a critical level.', 10)
					return
				end

				local carried = self.carriedCargo[gr:getID()] or 0
				if amount > zn.resource then
					amount = zn.resource
				end
				
				zn:removeResource(amount)
				zn:refreshText()
				self.carriedCargo[gr:getID()] = carried + amount
				self.lastLoaded[gr:getID()] = zn
				local onboard = self.carriedCargo[gr:getID()]
				local weight = self.getWeight(onboard)

				if un:getDesc().typeName == "Hercules" then
					local loadedInCrates = 0
					local ammo = un:getAmmo()
					if ammo then 
						for _,load in ipairs(ammo) do
							if load.desc.typeName == 'weapons.bombs.Generic Crate [20000lb]' then
								loadedInCrates = 9000 * load.count
							end
						end
					end

					local internal = 0
					if weight > loadedInCrates then
						internal = weight - loadedInCrates
					end

					trigger.action.setUnitInternalCargo(un:getName(), internal)
				else
					trigger.action.setUnitInternalCargo(un:getName(), weight)
				end

				trigger.action.outTextForUnit(un:getID(), amount..' supplies loaded', 10)
				trigger.action.outTextForUnit(un:getID(), onboard..' supplies onboard. ('..weight..' kg)', 10)
			end
		end
	end
	
	function PlayerLogistics:unloadSupplies(params)
		if not ZoneCommand then return end
		
		local groupName = params.group
		local amount = params.amount
		
		local gr = Group.getByName(groupName)
		if gr then
			local un = gr:getUnit(1)
			if un then
				if Utils.isInAir(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not unload supplies while in air', 10)
					return
				end
				
				local zn = ZoneCommand.getZoneOfUnit(un:getName())
				if not zn then 
					zn = CarrierCommand.getCarrierOfUnit(un:getName())
				end
				
				if not zn then
					trigger.action.outTextForUnit(un:getID(), 'Can only unload supplies while within a friendly zone', 10)
					return
				end
				
				if zn.side ~= 0 and zn.side ~= un:getCoalition()then
					trigger.action.outTextForUnit(un:getID(), 'Can only unload supplies while within a friendly zone', 10)
					return
				end

				if not self:isCargoDoorOpen(un) then
					trigger.action.outTextForUnit(un:getID(), 'Can not unload supplies while cargo door closed', 10)
					return
				end

				if not self.carriedCargo[gr:getID()] or self.carriedCargo[gr:getID()] == 0 then
					trigger.action.outTextForUnit(un:getID(), 'No supplies loaded', 10)
					return
				end
				
				local carried = self.carriedCargo[gr:getID()]
				if amount > carried then
					amount = carried
				end
				
				self.carriedCargo[gr:getID()] = carried-amount
				zn:addResource(amount)

				local lastLoad = self.lastLoaded[gr:getID()]
				self:awardSupplyXP(lastLoad, zn, un, amount)

				zn:refreshText()
				local onboard = self.carriedCargo[gr:getID()]
				local weight = self.getWeight(onboard)
			
				if un:getDesc().typeName == "Hercules" then
					local loadedInCrates = 0
					local ammo = un:getAmmo()
					for _,load in ipairs(ammo) do
						if load.desc.typeName == 'weapons.bombs.Generic Crate [20000lb]' then
							loadedInCrates = 9000 * load.count
						end
					end

					local internal = 0
					if weight > loadedInCrates then
						internal = weight - loadedInCrates
					end
					
					trigger.action.setUnitInternalCargo(un:getName(), internal)
				else
					trigger.action.setUnitInternalCargo(un:getName(), weight)
				end

				trigger.action.outTextForUnit(un:getID(), amount..' supplies unloaded', 10)
				trigger.action.outTextForUnit(un:getID(), onboard..' supplies remaining onboard. ('..weight..' kg)', 10)
			end
		end
	end
end

-----------------[[ END OF PlayerLogistics.lua ]]-----------------



-----------------[[ GroupMonitor.lua ]]-----------------

GroupMonitor = {}
do
	GroupMonitor.blockedDespawnTime = 10*60 --used to despawn aircraft that are stuck taxiing for some reason
	GroupMonitor.landedDespawnTime = 10
	GroupMonitor.atDestinationDespawnTime = 2*60
	GroupMonitor.recoveryReduction = 0.8 -- reduce recovered resource from landed missions by this amount to account for maintenance

	GroupMonitor.siegeExplosiveTime = 5*60 -- how long until random upgrade is detonated in zone
	GroupMonitor.siegeExplosiveStrength = 1000 -- detonation strength

	GroupMonitor.timeBeforeSquadDeploy = 10*60
	GroupMonitor.squadChance = 0.001
	GroupMonitor.ambushChance = 0.7

	GroupMonitor.aiSquads = {
		ambush = {
			[1] = {
				name='ambush-squad-red', 
				type=PlayerLogistics.infantryTypes.ambush,
				weight = 900,
				cost= 300,
				jobtime= 60*60*2,
				extracttime= 0,
				size = 5,
				side = 1,
				isAISpawned = true
			},
			[2] = {
				name='ambush-squad', 
				type=PlayerLogistics.infantryTypes.ambush,
				weight = 900,
				cost= 300,
				jobtime= 60*60,
				extracttime= 60*30,
				size = 5,
				side = 2,
				isAISpawned = true
			},
		},
		manpads = {
			[1] = {
				name='manpads-squad-red',
				type=PlayerLogistics.infantryTypes.manpads, 
				weight = 900,
				cost= 500,
				jobtime= 60*60*2,
				extracttime= 0,
				size = 5, 
				side= 1,
				isAISpawned = true
			},
			[2] = {
				name='manpads-squad',
				type=PlayerLogistics.infantryTypes.manpads, 
				weight = 900,
				cost= 500,
				jobtime= 60*60,
				extracttime= 60*30,
				size = 5, 
				side= 2,
				isAISpawned = true
			}
		}
	}

	function GroupMonitor:new()
		local obj = {}
		obj.groups = {}
		setmetatable(obj, self)
		self.__index = self
		
		obj:start()

		DependencyManager.register("GroupMonitor", obj)
		return obj
	end

	function GroupMonitor.isAirAttack(misType)
		if misType == ZoneCommand.missionTypes.cas then return true end
		if misType == ZoneCommand.missionTypes.cas_helo then return true end
		if misType == ZoneCommand.missionTypes.strike then return true end
		if misType == ZoneCommand.missionTypes.patrol then return true end
		if misType == ZoneCommand.missionTypes.sead then return true end
		if misType == ZoneCommand.missionTypes.bai then return true end
	end

	function GroupMonitor.hasWeapons(group)
		for _,un in ipairs(group:getUnits()) do
			local wps = un:getAmmo()
			if wps then
				for _,w in ipairs(wps) do
					if w.desc.category ~= 0 and w.count > 0 then
						return true
					end
				end
			end
		end
	end

	function GroupMonitor:sendHome(trackedGroup)
		if trackedGroup.home == nil then 
			env.info("GroupMonitor - sendHome "..trackedGroup.name..' does not have home set')
			return
		end

		if trackedGroup.returning then return end


		local gr = Group.getByName(trackedGroup.name)
		if gr then
			if trackedGroup.product.missionType == ZoneCommand.missionTypes.cas_helo then
				local hsp = trigger.misc.getZone(trackedGroup.home.name..'-hsp')
				if not hsp then
					hsp = trigger.misc.getZone(trackedGroup.home.name)
				end

				local alt = DependencyManager.get("ConnectionManager"):getHeliAlt(trackedGroup.target.name, trackedGroup.home.name)
				TaskExtensions.landAtPointFromAir(gr, {x=hsp.point.x, y=hsp.point.z}, alt)
			else
				local homeZn = trigger.misc.getZone(trackedGroup.home.name)
				TaskExtensions.landAtAirfield(gr, {x=homeZn.point.x, y=homeZn.point.z})
			end
			
			local cnt = gr:getController()
			cnt:setOption(0,4) -- force ai hold fire
			cnt:setOption(1, 4) -- force reaction on threat to allow abort
			
			trackedGroup.returning = true
			env.info('GroupMonitor - sendHome ['..trackedGroup.name..'] returning home')
		end
	end
	
	function GroupMonitor:registerGroup(product, target, home, savedData)
		self.groups[product.name] = {name = product.name, lastStateTime = timer.getAbsTime(), product = product, target = target, home = home, stuck_marker = 0}

		if savedData and savedData.state ~= 'uninitialized' then
			env.info('GroupMonitor - registerGroup ['..product.name..'] restored state '..savedData.state..' dur:'..savedData.lastStateDuration)
			self.groups[product.name].state = savedData.state
			self.groups[product.name].lastStateTime = timer.getAbsTime() - savedData.lastStateDuration
			self.groups[product.name].spawnedSquad = savedData.spawnedSquad
		end
	end
	
	function GroupMonitor:start()
		timer.scheduleFunction(function(param, time)
			local self = param.context
			
			for i,v in pairs(self.groups) do
				local isDead = false
				if v.product.missionType == 'supply_convoy' or v.product.missionType == 'assault' then
					isDead = self:processSurface(v)
					if isDead then 
						MissionTargetRegistry.removeBaiTarget(v) --safety measure in case group is dead
					end
				else
					isDead = self:processAir(v)
				end
				
				if isDead then
					self.groups[i] = nil
				end
			end
			
			return time+10
		end, {context = self}, timer.getTime()+1)
	end

	function GroupMonitor:getGroup(name)
		return self.groups[name]
	end
	
	function GroupMonitor:processSurface(group) -- states: [started, enroute, atdestination, siege]
		local gr = Group.getByName(group.name)
		if not gr then return true end
		if gr:getSize()==0 then 
			gr:destroy()
			return true
		end
		
		if not group.state then 
			group.state = 'started'
			group.lastStateTime = timer.getAbsTime()
			env.info('GroupMonitor: processSurface ['..group.name..'] starting')
		end
		
		if group.state =='started' then
			if gr then
				local firstUnit = gr:getUnit(1):getName()
				local z = ZoneCommand.getZoneOfUnit(firstUnit)
				if not z then 
					z = CarrierCommand.getCarrierOfUnit(firstUnit)
				end
				
				if not z then
					env.info('GroupMonitor: processSurface ['..group.name..'] is enroute')
					group.state = 'enroute'
					group.lastStateTime = timer.getAbsTime()
					MissionTargetRegistry.addBaiTarget(group)
				elseif timer.getAbsTime() - group.lastStateTime > GroupMonitor.blockedDespawnTime then
					env.info('GroupMonitor: processSurface ['..group.name..'] despawned due to blockage')
					gr:destroy()
					local todeliver = math.floor(group.product.cost)
					z:addResource(todeliver)
					return true
				end
			end
		elseif group.state =='enroute' then
			if gr then
				local firstUnit = gr:getUnit(1):getName()
				local z = ZoneCommand.getZoneOfUnit(firstUnit)
				if not z then 
					z = CarrierCommand.getCarrierOfUnit(firstUnit)
				end
				
				if z and (z.name==group.target.name or z.name==group.home.name) then
					MissionTargetRegistry.removeBaiTarget(group)
					
					if group.product.missionType == 'supply_convoy' then
						env.info('GroupMonitor: processSurface ['..group.name..'] has arrived at destination')
						group.state = 'atdestination'
						group.lastStateTime = timer.getAbsTime()
						z:capture(gr:getCoalition())
						local percentSurvived = gr:getSize()/gr:getInitialSize()
						local todeliver = math.floor(group.product.cost * percentSurvived)
						z:addResource(todeliver)
						env.info('GroupMonitor: processSurface ['..group.name..'] has supplied ['..z.name..'] with ['..todeliver..']')
					elseif group.product.missionType == 'assault' then
						if z.side == gr:getCoalition() then
							env.info('GroupMonitor: processSurface ['..group.name..'] has arrived at destination')
							group.state = 'atdestination'
							group.lastStateTime = timer.getAbsTime()
							local percentSurvived = gr:getSize()/gr:getInitialSize()
							local torecover = math.floor(group.product.cost * percentSurvived * GroupMonitor.recoveryReduction)
							z:addResource(torecover)
							env.info('GroupMonitor: processSurface ['..z.name..'] has recovered ['..torecover..'] from ['..group.name..']')
						elseif z.side == 0 then
							env.info('GroupMonitor: processSurface ['..group.name..'] has arrived at destination')
							group.state = 'atdestination'
							group.lastStateTime = timer.getAbsTime()
							z:capture(gr:getCoalition())
							env.info('GroupMonitor: processSurface ['..group.name..'] has captured ['..z.name..']')
						elseif z.side ~= gr:getCoalition() and z.side ~= 0  then
							env.info('GroupMonitor: processSurface ['..group.name..'] starting siege')
							group.state = 'siege'
							group.lastStateTime = timer.getAbsTime()
						end
					end
				else
					if group.product.missionType == 'supply_convoy' then
						if not group.returning and group.target and group.target.side ~= group.product.side and group.target.side ~= 0 then
							local supplyPoint = trigger.misc.getZone(group.home.name..'-sp')
							if not supplyPoint then
								supplyPoint = trigger.misc.getZone(group.home.name)
							end
	
							if supplyPoint then 
								group.returning = true
								env.info('GroupMonitor: processSurface ['..group.name..'] returning home')
								TaskExtensions.moveOnRoadToPoint(gr,  {x=supplyPoint.point.x, y=supplyPoint.point.z})
							end
						elseif GroupMonitor.isStuck(group) then
							env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck')

							local tgtname = group.target.name
							if group.returning then 
								tgtname = group.home.name
							end

							local supplyPoint = trigger.misc.getZone(tgtname..'-sp')
							if not supplyPoint then
								supplyPoint = trigger.misc.getZone(tgtname)
							end
							TaskExtensions.moveOnRoadToPoint(gr,  {x=supplyPoint.point.x, y=supplyPoint.point.z}, true)
							
							group.unstuck_attempts = group.unstuck_attempts or 0
							group.unstuck_attempts = group.unstuck_attempts + 1

							if group.unstuck_attempts >= 5 then
								env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck by teleport')
								group.unstuck_attempts = 0
								local frUnit = gr:getUnit(1)
								local pos = frUnit:getPoint()

								mist.teleportToPoint({
									groupName = group.name,
									action = 'teleport',
									initTasks = false,
									point = {x=pos.x+math.random(-25,25), y=pos.y, z = pos.z+math.random(-25,25)}
								})

								timer.scheduleFunction(function(params, time)
									local group = params.gr
									local tgtname = group.target.name
									if group.returning then 
										tgtname = group.home.name
									end
									local gr = Group.getByName(group.name)
									local supplyPoint = trigger.misc.getZone(tgtname..'-sp')
									if not supplyPoint then
										supplyPoint = trigger.misc.getZone(tgtname)
									end

									TaskExtensions.moveOnRoadToPoint(gr,  {x=supplyPoint.point.x, y=supplyPoint.point.z}, true)
								end, {gr = group}, timer.getTime()+2)
							end
						end
					elseif group.product.missionType == 'assault' then
						local frUnit = gr:getUnit(1)
						if frUnit then
							local skipDetection = false
							if group.lastStarted and (timer.getAbsTime() - group.lastStarted) < (30) then
								skipDetection = true
							else
								group.lastStarted = nil
							end

							local shouldstop = false
							if not skipDetection then
								local controller = frUnit:getController()
								local targets = controller:getDetectedTargets()

								if #targets > 0 then
									for _,tgt in ipairs(targets) do
										if tgt.visible and tgt.object then
											if tgt.object.isExist and tgt.object:isExist() and tgt.object.getCoalition and tgt.object:getCoalition()~=frUnit:getCoalition() and 
												Object.getCategory(tgt.object) == 1 then
												local dist = mist.utils.get3DDist(frUnit:getPoint(), tgt.object:getPoint())
												if dist < 700 then
													if not group.isstopped then
														env.info('GroupMonitor: processSurface ['..group.name..'] stopping to engage targets')
														TaskExtensions.stopAndDisperse(gr)
														group.isstopped = true
														group.lastStopped = timer.getAbsTime()
													end
													shouldstop = true
													break
												end
											end
										end
									end
								end
							end

							if group.lastStopped then
								if (timer.getAbsTime() - group.lastStopped) > (3*60) then
								env.info('GroupMonitor: processSurface ['..group.name..'] override stop, waited too long')
									shouldstop = false
									group.lastStarted = timer.getAbsTime()
								end
							end

							if not shouldstop and group.isstopped then
								env.info('GroupMonitor: processSurface ['..group.name..'] resuming mission')
								local tp = {
									x = group.target.zone.point.x,
									y = group.target.zone.point.z
								}

								TaskExtensions.moveOnRoadToPointAndAssault(gr, tp, group.target.built)
								group.isstopped = false
								group.lastStopped = nil
							end

							if not shouldstop and not group.isstopped then
								if GroupMonitor.isStuck(group) then
									env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck')
									local tp = {
										x = group.target.zone.point.x,
										y = group.target.zone.point.z
									}

									TaskExtensions.moveOnRoadToPointAndAssault(gr, tp, group.target.built, true)

									group.unstuck_attempts = group.unstuck_attempts or 0
									group.unstuck_attempts = group.unstuck_attempts + 1

									if group.unstuck_attempts >= 5 then
										env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck by teleport')
										group.unstuck_attempts = 0
										local pos = frUnit:getPoint()

										mist.teleportToPoint({
											groupName = group.name,
											action = 'teleport',
											initTasks = false,
											point = {x=pos.x+math.random(-25,25), y=pos.y, z = pos.z+math.random(-25,25)}
										})

										timer.scheduleFunction(function(params, time)
											local group = params.group
											local gr = Group.getByName(gr)
											local tp = {
												x = group.target.zone.point.x,
												y = group.target.zone.point.z
											}
		
											TaskExtensions.moveOnRoadToPointAndAssault(gr, tp, group.target.built, true)
										end, {gr = group}, timer.getTime()+2)
									end
								elseif group.unstuck_attempts and group.unstuck_attempts > 0 then
									group.unstuck_attempts = 0
								end
							end

							local timeElapsed = timer.getAbsTime() - group.lastStateTime
							if not group.spawnedSquad and timeElapsed > GroupMonitor.timeBeforeSquadDeploy then
								local die = math.random()
								if die < GroupMonitor.squadChance then
									local pos = gr:getUnit(1):getPoint()

									local squadData = nil
									if math.random() > GroupMonitor.ambushChance then
										squadData = GroupMonitor.aiSquads.manpads[gr:getCoalition()]
									else
										squadData = GroupMonitor.aiSquads.ambush[gr:getCoalition()]
									end

									DependencyManager.get("SquadTracker"):spawnInfantry(squadData, pos)
									env.info('GroupMonitor: processSurface ['..group.name..'] has deployed '..squadData.type..' squad')
									group.spawnedSquad = true
								end
							end
						end
					end
				end
			end
		elseif group.state == 'atdestination' then
			if timer.getAbsTime() - group.lastStateTime > GroupMonitor.atDestinationDespawnTime then
				
				if gr then
					local firstUnit = gr:getUnit(1):getName()
					local z = ZoneCommand.getZoneOfUnit(firstUnit)
					if not z then 
						z = CarrierCommand.getCarrierOfUnit(firstUnit)
					end
					if z and z.side == 0 then
						env.info('GroupMonitor: processSurface ['..group.name..'] is at neutral zone')
						z:capture(gr:getCoalition())
						env.info('GroupMonitor: processSurface ['..group.name..'] has captured ['..z.name..']')
					else
						env.info('GroupMonitor: processSurface ['..group.name..'] starting siege')
						group.state = 'siege'
						group.lastStateTime = timer.getAbsTime()
					end

					env.info('GroupMonitor: processSurface ['..group.name..'] despawned after arriving at destination')
					gr:destroy()
					return true
				end
			end
		elseif group.state == 'siege' then
			if group.product.missionType ~= 'assault' then 
				group.state = 'atdestination'
				group.lastStateTime = timer.getAbsTime()
			else
				if timer.getAbsTime() - group.lastStateTime > GroupMonitor.siegeExplosiveTime then
					if gr then
						local firstUnit = gr:getUnit(1):getName()
						local z = ZoneCommand.getZoneOfUnit(firstUnit)
						local success = false
						
						if z then
							for i,v in pairs(z.built) do
								if v.type == 'upgrade' and v.side ~= gr:getCoalition() then
									local st = StaticObject.getByName(v.name)
									if not st then st = Group.getByName(v.name) end
									local pos = st:getPoint()
									trigger.action.explosion(pos, GroupMonitor.siegeExplosiveStrength)
									group.lastStateTime = timer.getAbsTime()
									success = true
									env.info('GroupMonitor: processSurface ['..group.name..'] detonating structure at '..z.name)
									break
								end
							end
						end

						if not success then
							env.info('GroupMonitor: processSurface ['..group.name..'] no targets to detonate, switching to atdestination')
							group.state = 'atdestination'
							group.lastStateTime = timer.getAbsTime()
						end
					end
				end
			end
		end
	end

	function GroupMonitor.isStuck(group)
		local gr = Group.getByName(group.name)
		if not gr then return false end
		if gr:getSize() == 0 then return false end

		local un = gr:getUnit(1)
		if un and un:isExist() and mist.vec.mag(un:getVelocity()) >= 0.01 and group.stuck_marker > 0 then
			group.stuck_marker = 0
			group.unstuck_attempts = 0
			env.info('GroupMonitor: isStuck ['..group.name..'] is moving, reseting stuck marker velocity='..mist.vec.mag(un:getVelocity()))
		end

		if un and un:isExist() and mist.vec.mag(un:getVelocity()) < 0.01 then
			group.stuck_marker = group.stuck_marker + 1
			env.info('GroupMonitor: isStuck ['..group.name..'] is not moving, increasing stuck marker to '..group.stuck_marker..' velocity='..mist.vec.mag(un:getVelocity()))

			if group.stuck_marker >= 3 then
				group.stuck_marker = 0
				env.info('GroupMonitor: isStuck ['..group.name..'] is stuck')
				return true
			end
		end

		return false
	end
	
	function GroupMonitor:processAir(group)-- states: [takeoff, inair, landed]
		local gr = Group.getByName(group.name)
		if not gr then return true end
		if not gr:isExist() or gr:getSize()==0 then 
			gr:destroy()
			return true
		end
		--[[
		if group.product.missionType == 'cas' or group.product.missionType == 'cas_helo' or group.product.missionType == 'strike' or group.product.missionType == 'sead' then
			if MissionTargetRegistry.isZoneTargeted(group.target) and group.product.side == 2 and not group.returning then 
				env.info('GroupMonitor - mission ['..group.name..'] to ['..group.target..'] canceled due to player mission')

				GroupMonitor.sendHome(group)
			end
		end
		]]--
		
		if not group.state then 
			group.state = 'takeoff' 
			env.info('GroupMonitor: processAir ['..group.name..'] taking off')
		end
		
		if group.state =='takeoff' then
			if timer.getAbsTime() - group.lastStateTime > GroupMonitor.blockedDespawnTime then
				if gr and gr:getSize()>0 and gr:getUnit(1):isExist() then
					local frUnit = gr:getUnit(1)
					local cz = CarrierCommand.getCarrierOfUnit(frUnit:getName())
					if Utils.allGroupIsLanded(gr, cz ~= nil) then
						env.info('GroupMonitor: processAir ['..group.name..'] is blocked, despawning')
						local frUnit = gr:getUnit(1)
						if frUnit then
							local firstUnit = frUnit:getName()
							local z = ZoneCommand.getZoneOfUnit(firstUnit)
							if not z then 
								z = CarrierCommand.getCarrierOfUnit(firstUnit)
							end
							if z then
								z:addResource(group.product.cost)
								env.info('GroupMonitor: processAir ['..z.name..'] has recovered ['..group.product.cost..'] from ['..group.name..']')
							end
						end

						gr:destroy()
						return true
					end
				end
			elseif gr and Utils.someOfGroupInAir(gr) then
				env.info('GroupMonitor: processAir ['..group.name..'] is in the air')
				group.state = 'inair'
				group.lastStateTime = timer.getAbsTime()
			end
		elseif group.state =='inair' then
			if gr then
				local unit = gr:getUnit(1)
				if not unit or not unit:isExist() then return end
				
				local cz = CarrierCommand.getCarrierOfUnit(unit:getName())
				if Utils.allGroupIsLanded(gr, cz ~= nil) then
					env.info('GroupMonitor: processAir ['..group.name..'] has landed')
					group.state = 'landed'
					group.lastStateTime = timer.getAbsTime()

					if unit then
						local firstUnit = unit:getName()
						local z = ZoneCommand.getZoneOfUnit(firstUnit)
						if not z then 
							z = CarrierCommand.getCarrierOfUnit(firstUnit)
						end
						
						if group.product.missionType == 'supply_air' then
							if z then
								z:capture(gr:getCoalition())
								z:addResource(group.product.cost)
								env.info('GroupMonitor: processAir ['..group.name..'] has supplied ['..z.name..'] with ['..group.product.cost..']')
							end
						else
							if z and z.side == gr:getCoalition() then
								local percentSurvived = gr:getSize()/gr:getInitialSize()
								local torecover = math.floor(group.product.cost * percentSurvived * GroupMonitor.recoveryReduction)
								z:addResource(torecover)
								env.info('GroupMonitor: processAir ['..z.name..'] has recovered ['..torecover..'] from ['..group.name..']')
							end
						end
					else
						env.info('GroupMonitor: processAir ['..group.name..'] size ['..gr:getSize()..'] has no unit 1')
					end
				else
					if GroupMonitor.isAirAttack(group.product.missionType) and not group.returning then
						if not GroupMonitor.hasWeapons(gr) then
							env.info('GroupMonitor: processAir ['..group.name..'] size ['..gr:getSize()..'] has no weapons outside of shells')
							self:sendHome(group)
						elseif group.product.missionType == ZoneCommand.missionTypes.cas_helo then 
							local frUnit = gr:getUnit(1)
							local controller = frUnit:getController()
							local targets = controller:getDetectedTargets()

							local tgtToEngage = {}
							if #targets > 0 then
								for _,tgt in ipairs(targets) do
									if tgt.visible and tgt.object and tgt.object.isExist and tgt.object:isExist() then
										if Object.getCategory(tgt.object) == Object.Category.UNIT and 
											tgt.object.getCoalition and tgt.object:getCoalition()~=frUnit:getCoalition() and 
											Unit.getCategoryEx(tgt.object) == Unit.Category.GROUND_UNIT then

											local dist = mist.utils.get3DDist(frUnit:getPoint(), tgt.object:getPoint())
											if dist < 2000 then
												table.insert(tgtToEngage, tgt.object)
											end
										end
									end
								end
							end

							if not group.isengaging and #tgtToEngage > 0 then
								env.info('GroupMonitor: processAir ['..group.name..'] engaging targets')
								TaskExtensions.heloEngageTargets(gr, tgtToEngage, group.product.expend)
								group.isengaging = true
								group.startedEngaging = timer.getAbsTime()
							elseif group.isengaging and #tgtToEngage == 0 and group.startedEngaging and (timer.getAbsTime() - group.startedEngaging) > 60*5 then
								env.info('GroupMonitor: processAir ['..group.name..'] resuming mission')
								if group.returning then
									group.returning = nil
									self:sendHome(group)
								else
									local homePos = group.home.zone.point
									TaskExtensions.executeHeloCasMission(gr, group.target.built, group.product.expend, group.product.altitude, {homePos = homePos})
								end
								group.isengaging = false
							end
						end
					elseif group.product.missionType == 'supply_air' then
						if not group.returning and group.target and group.target.side ~= group.product.side and group.target.side ~= 0 then
							local supplyPoint = trigger.misc.getZone(group.home.name..'-hsp')
							if not supplyPoint then
								supplyPoint = trigger.misc.getZone(group.home.name)
							end

							if supplyPoint then 
								group.returning = true
								local alt = DependencyManager.get("ConnectionManager"):getHeliAlt(group.target.name, group.home.name)
								TaskExtensions.landAtPointFromAir(gr,  {x=supplyPoint.point.x, y=supplyPoint.point.z}, alt)
								env.info('GroupMonitor: processAir ['..group.name..'] returning home')
							end
						end
					end
				end
			end
		elseif group.state =='landed' then
			if timer.getAbsTime() - group.lastStateTime > GroupMonitor.landedDespawnTime then
				if gr then
					env.info('GroupMonitor: processAir ['..group.name..'] despawned after landing')
					gr:destroy()
					return true
				end
			end
		end
	end
end

-----------------[[ END OF GroupMonitor.lua ]]-----------------



-----------------[[ ConnectionManager.lua ]]-----------------

ConnectionManager = {}
do
	ConnectionManager.currentLineIndex = 5000
	function ConnectionManager:new()
		local obj = {}
		obj.connections = {}
		obj.zoneConnections = {}
		obj.heliAlts = {}
		obj.blockedRoads = {}
		setmetatable(obj, self)
		self.__index = self

		DependencyManager.register("ConnectionManager", obj)
		return obj
	end

	function ConnectionManager:addConnection(f, t, blockedRoad, heliAlt)
		local i = ConnectionManager.currentLineIndex
		ConnectionManager.currentLineIndex = ConnectionManager.currentLineIndex + 1
		
		table.insert(self.connections, {from=f, to=t, index=i})
		self.zoneConnections[f] = self.zoneConnections[f] or {}
		self.zoneConnections[t] = self.zoneConnections[t] or {}
		self.zoneConnections[f][t] = true
		self.zoneConnections[t][f] = true
		
		if heliAlt then
			self.heliAlts[f] = self.heliAlts[f] or {}
			self.heliAlts[t] = self.heliAlts[t] or {}
			self.heliAlts[f][t] = heliAlt
			self.heliAlts[t][f] = heliAlt
		end

		if blockedRoad then
			self.blockedRoads[f] = self.blockedRoads[f] or {}
			self.blockedRoads[t] = self.blockedRoads[t] or {}
			self.blockedRoads[f][t] = true
			self.blockedRoads[t][f] = true
		end

		local from = CustomZone:getByName(f)
		local to = CustomZone:getByName(t)

		if not from then env.info("ConnectionManager - addConnection: missing zone "..f) end
		if not to then env.info("ConnectionManager - addConnection: missing zone "..t) end
		
		if blockedRoad then
			trigger.action.lineToAll(-1, i, from.point, to.point, {1,1,1,0.5}, 3)
		else
			trigger.action.lineToAll(-1, i, from.point, to.point, {1,1,1,0.5}, 2)
		end
	end
	
	function ConnectionManager:getConnectionsOfZone(zonename)
		if not self.zoneConnections[zonename] then return {} end
		
		local connections = {}
		for i,v in pairs(self.zoneConnections[zonename]) do
			table.insert(connections, i)
		end
		
		return connections
	end

	function ConnectionManager:isRoadBlocked(f,t)
		if self.blockedRoads[f] then 
			return self.blockedRoads[f][t]
		end

		if self.blockedRoads[t] then 
			return self.blockedRoads[t][f]
		end
	end

	function ConnectionManager:getHeliAltSimple(f,t)
		if self.heliAlts[f] then 
			if self.heliAlts[f][t] then
				return self.heliAlts[f][t]
			end
		end

		if self.heliAlts[t] then 
			if self.heliAlts[t][f] then
				return self.heliAlts[t][f]
			end
		end
	end

	function ConnectionManager:getHeliAlt(f,t)
		local alt = self:getHeliAltSimple(f,t)
		if alt then return alt end

		if self.heliAlts[f] then 
			local max = -1
			for zn,_ in pairs(self.heliAlts[f]) do
				local alt = self:getHeliAltSimple(f, zn)
				if alt then
					if alt > max then
						max = alt
					end
				end

				alt = self:getHeliAltSimple(zn, t)
				if alt then
					if alt > max then
						max = alt
					end
				end
			end
			
			if max > 0 then return max end
		end

		if self.heliAlts[t] then 
			local max = -1
			for zn,_ in pairs(self.heliAlts[t]) do
				local alt = self:getHeliAltSimple(t, zn)
				if alt then
					if alt > max then
						max = alt
					end
				end

				alt = self:getHeliAltSimple(zn, f)
				if alt then
					if alt > max then
						max = alt
					end
				end
			end

			if max > 0 then return max end
		end
	end
end

-----------------[[ END OF ConnectionManager.lua ]]-----------------



-----------------[[ TaskExtensions.lua ]]-----------------

TaskExtensions = {}
do
	function TaskExtensions.getAttackTask(targetName, expend, altitude)
		local tgt = Group.getByName(targetName)
		if tgt then
			return { 
				id = 'AttackGroup', 
				params = { 
					groupId = tgt:getID(),
					expend = expend,
					weaponType = Weapon.flag.AnyWeapon,
					groupAttack = true,
					altitudeEnabled = (altitude ~= nil),
					altitude = altitude
				} 
			}
		else
			tgt = StaticObject.getByName(targetName)
			if not tgt then tgt = Unit.getByName(targetName) end
			if tgt then
				return { 
					id = 'AttackUnit', 
					params = { 
						unitId = tgt:getID(),
						expend = expend,
						weaponType = Weapon.flag.AnyWeapon,
						groupAttack = true,
						altitudeEnabled = (altitude ~= nil),
						altitude = altitude
					} 
				}
			end
		end
	end

	function TaskExtensions.getTargetPos(targetName)
		local tgt = StaticObject.getByName(targetName)
		if not tgt then tgt = Unit.getByName(targetName) end
		if tgt then
			return tgt:getPoint()
		end
	end

	function TaskExtensions.getDefaultWaypoints(startPos, task, tgpos, reactivated, landUnitID)
		local defwp = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {}
				}  
			}
		}

		if reactivated then
			table.insert(defwp.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = reactivated.currentPos.x,
				y = reactivated.currentPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FLY_OVER_POINT,
				alt = 4572,
				alt_type = AI.Task.AltitudeType.BARO, 
				task = task
			})
		else
			table.insert(defwp.params.route.points, {
				type= AI.Task.WaypointType.TAKEOFF,
				x = startPos.x,
				y = startPos.z,
				speed = 0,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})

			table.insert(defwp.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = startPos.x,
				y = startPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FLY_OVER_POINT,
				alt = 4572,
				alt_type = AI.Task.AltitudeType.BARO, 
				task = task
			})
		end

		if tgpos then
			table.insert(defwp.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = tgpos.x,
				y = tgpos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FLY_OVER_POINT,
				alt = 4572,
				alt_type = AI.Task.AltitudeType.BARO,
				task = task
			})
		end

		if landUnitID then
			table.insert(defwp.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				linkUnit = landUnitID,
				helipadId = landUnitID,
				x = startPos.x,
				y = startPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		else
			table.insert(defwp.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				x = startPos.x,
				y = startPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		end

		return defwp
	end

	function TaskExtensions.executeSeadMission(group,targets, expend, altitude, reactivated)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end

		local expCount = AI.Task.WeaponExpend.ALL
		if expend then
			expCount = expend
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		local viable = {}
		for i,v in pairs(targets) do
			if v.type == 'defense' and v.side ~= group:getCoalition() then
				local gr = Group.getByName(v.name)
				for _,unit in ipairs(gr:getUnits()) do
					if unit:hasAttribute('SAM SR') or unit:hasAttribute('SAM TR') then
						table.insert(viable, unit:getName())
					end
				end
			end
		end

		local attack = {
			id = 'ComboTask',
			params = {
				tasks = {
					{ 
						id = 'EngageTargets', 
						params = {  
						  targetTypes = {'SAM SR', 'SAM TR'}
						} 
					}
				}
			}
		}

		for i,v in ipairs(viable) do
			local task = TaskExtensions.getAttackTask(v, expCount, alt)
			table.insert(attack.params.tasks, task)
		end

		local firstunitpos = nil
		local tgt = viable[1]
		if tgt then 
			firstunitpos = Unit.getByName(tgt):getPoint()
		end
		
		local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, firstunitpos, reactivated)

		group:getController():setTask(mis)
		TaskExtensions.setDefaultAG(group)
	end

	function TaskExtensions.executeStrikeMission(group, targets, expend, altitude, reactivated, landUnitID)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end

		local expCount = AI.Task.WeaponExpend.ALL
		if expend then
			expCount = expend
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		local attack = {
			id = 'ComboTask',
			params = {
				tasks = {
				}
			}
		}

		for i,v in pairs(targets) do
			if v.type == 'upgrade' and v.side ~= group:getCoalition() then
				local task = TaskExtensions.getAttackTask(v.name, expCount, alt)
				table.insert(attack.params.tasks, task)
			end
		end

		local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, nil, reactivated, landUnitID)

		group:getController():setTask(mis)
		TaskExtensions.setDefaultAG(group)
	end

	function TaskExtensions.executePinpointStrikeMission(group, targetPos, expend, altitude, reactivated, landUnitID)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end

		local expCount = AI.Task.WeaponExpend.ALL
		if expend then
			expCount = expend
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		local attack = {
			id = 'Bombing', 
			params = { 
				point = {
					x = targetPos.x, 
					y = targetPos.z
				},
				attackQty = 1,
				weaponType = Weapon.flag.AnyBomb,
				expend = expCount,
				groupAttack = true, 
				altitude = alt,
				altitudeEnabled = (altitude ~= nil),
			} 
		}

		local diff = {
			x = targetPos.x - startPos.x,
			z = targetPos.z - startPos.z
		}

		local tp = {
			x = targetPos.x - diff.x*0.5,
			z = targetPos.z - diff.z*0.5
		}

		local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, tp, reactivated, landUnitID)

		group:getController():setTask(mis)
		TaskExtensions.setDefaultAG(group)
	end

	function TaskExtensions.executeCasMission(group, targets, expend, altitude, reactivated)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end

		local attack = {
			id = 'ComboTask',
			params = {
				tasks = {
				}
			}
		}

		local expCount = AI.Task.WeaponExpend.ONE
		if expend then
			expCount = expend
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		for i,v in pairs(targets) do
			if v.type == 'defense' then
				local g = Group.getByName(i)
				if g and g:getCoalition()~=group:getCoalition() then
					local task = TaskExtensions.getAttackTask(i, expCount, alt)
					table.insert(attack.params.tasks, task)
				end
			end
		end

		local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, nil, reactivated)

		group:getController():setTask(mis)
		TaskExtensions.setDefaultAG(group)
	end

	function TaskExtensions.executeBaiMission(group, targets, expend, altitude, reactivated)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end

		local attack = {
			id = 'ComboTask',
			params = {
				tasks = {
					{ 
						id = 'EngageTargets', 
						params = {  
						  targetTypes = {'Vehicles'}
						} 
					}
				}
			}
		}

		local expCount = AI.Task.WeaponExpend.ONE
		if expend then
			expCount = expend
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		for i,v in pairs(targets) do
			if v.type == 'mission' and (v.missionType == 'assault' or v.missionType == 'supply_convoy') then
				local g = Group.getByName(i)
				if g and g:getSize()>0 and g:getCoalition()~=group:getCoalition() then
					local task = TaskExtensions.getAttackTask(i, expCount, alt)
					table.insert(attack.params.tasks, task)
				end
			end
		end

		local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, nil, reactivated)

		group:getController():setTask(mis)
		TaskExtensions.setDefaultAG(group)
	end

	function TaskExtensions.heloEngageTargets(group, targets, expend)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		local attack = {
			id = 'ComboTask',
			params = {
				tasks = {
				}
			}
		}

		local expCount = AI.Task.WeaponExpend.ONE
		if expend then
			expCount = expend
		end
		
		for i,v in pairs(targets) do
			local task = { 
				id = 'AttackUnit', 
				params = { 
					unitId = v:getID(),
					expend = expend,
					weaponType = Weapon.flag.AnyWeapon,
					groupAttack = true
				} 
			}

			table.insert(attack.params.tasks, task)
		end

		group:getController():pushTask(attack)
	end

	function TaskExtensions.executeHeloCasMission(group, targets, expend, altitude, reactivated)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end

		local attack = {
			id = 'ComboTask',
			params = {
				tasks = {
				}
			}
		}

		local expCount = AI.Task.WeaponExpend.ONE
		if expend then
			expCount = expend
		end
		
		local alt = 61
		if altitude then
			alt = altitude/3.281
		end

		for i,v in pairs(targets) do
			if v.type == 'defense' then
				local g = Group.getByName(i)
				if g and g:getCoalition()~=group:getCoalition() then
					local task = TaskExtensions.getAttackTask(i, expCount, alt)
					table.insert(attack.params.tasks, task)
				end
			end
		end

		local land = {
			id='Land',
			params = {
				point = {x = startPos.x, y=startPos.z}
			}
		}

		local mis = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {}
				}  
			}
		}

		if reactivated then
			table.insert(mis.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = reactivated.currentPos.x+1000,
				y = reactivated.currentPos.z+1000,
				speed = 257,
				action = AI.Task.TurnMethod.FLY_OVER_POINT,
				alt = alt,
				alt_type = AI.Task.AltitudeType.RADIO, 
				task = attack
			})
		else
			table.insert(mis.params.route.points, {
				type= AI.Task.WaypointType.TAKEOFF,
				x = startPos.x,
				y = startPos.z,
				speed = 0,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO,
			})

			table.insert(mis.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = startPos.x+1000,
				y = startPos.z+1000,
				speed = 257,
				action = AI.Task.TurnMethod.FLY_OVER_POINT,
				alt = alt,
				alt_type = AI.Task.AltitudeType.RADIO, 
				task = attack
			})
		end

		table.insert(mis.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = startPos.x,
			y = startPos.z,
			speed = 257,
			action = AI.Task.TurnMethod.FIN_POINT,
			alt = alt,
			alt_type = AI.Task.AltitudeType.RADIO, 
			task = land
		})
		
		group:getController():setTask(mis)
		TaskExtensions.setDefaultAG(group)
	end

	function TaskExtensions.executeTankerMission(group, point, altitude, frequency, tacan, reactivated, landUnitID)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()
		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		local freq = 259500000
		if frequency then
			freq = math.floor(frequency*1000000)
		end

		local setfreq = {
			id = 'SetFrequency', 
			params = { 
				frequency = freq,
				modulation = 0
			} 
		}

		local setbeacon = {
			id = 'ActivateBeacon', 
			params = { 
				type = 4, -- TACAN type
				system = 4, -- Tanker TACAN
				name = 'tacan task', 
				callsign = group:getUnit(1):getCallsign():sub(1,3), 
				frequency = tacan,
				AA = true,
				channel = tacan,
				bearing = true,
				modeChannel = "X"
			} 
		}

		local distFromPoint = 20000
		local theta = math.random() * 2 * math.pi
  
		local dx = distFromPoint * math.cos(theta)
		local dy = distFromPoint * math.sin(theta)

		local pos1 = {
			x = point.x + dx,
			y = point.z + dy
		}

		local pos2 = {
			x = point.x - dx,
			y = point.z - dy
		}

		local orbit_speed = 97
		local travel_speed = 450

		local orbit = { 
			id = 'Orbit', 
			params = { 
				pattern = AI.Task.OrbitPattern.RACE_TRACK,
				point = pos1,
   				point2 = pos2,
				speed = orbit_speed,
				altitude = alt
			}
		}

		local script = {
			id = "WrappedAction",
			params = {
				action = {
					id = "Script",
					params = 
					{
						command = "trigger.action.outTextForCoalition("..group:getCoalition()..", 'Tanker on station. "..(freq/1000000).." AM', 15)",
					}
				}
			}
		}

		local tanker = {
			id = 'Tanker', 
			params = { 
			}
		}

		local task = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {}
				}
			}
		}

		if reactivated then
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = pos1.x,
				y = pos1.y,
				speed = travel_speed,
				action = AI.Task.TurnMethod.FLY_OVER_POINT,
				alt = alt,
				alt_type = AI.Task.AltitudeType.BARO,
				task = tanker
			})
		else
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.TAKEOFF,
				x = startPos.x,
				y = startPos.z,
				speed = 0,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO,
				task = tanker
			})
		end

		table.insert(task.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = pos1.x,
			y = pos1.y,
			speed = orbit_speed,
			action = AI.Task.TurnMethod.FLY_OVER_POINT,
			alt = alt,
			alt_type = AI.Task.AltitudeType.BARO,
			task = {
				id = 'ComboTask',
				params = {
					tasks = {
						script
					}
				}
			}
		})

		table.insert(task.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = pos2.x,
			y = pos2.y,
			speed = orbit_speed,
			action = AI.Task.TurnMethod.FLY_OVER_POINT,
			alt = alt,
			alt_type = AI.Task.AltitudeType.BARO,
			task = {
				id = 'ComboTask',
				params = {
					tasks = {
						orbit
					}
				}
			}
		})

		if landUnitID then
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				linkUnit = landUnitID,
				helipadId = landUnitID,
				x = startPos.x,
				y = startPos.z,
				speed = travel_speed,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		else
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				x = startPos.x,
				y = startPos.z,
				speed = travel_speed,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		end
			
		group:getController():setTask(task)
		group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE)
		group:getController():setCommand(setfreq)
		group:getController():setCommand(setbeacon)
	end

	function TaskExtensions.executeAwacsMission(group, point, altitude, frequency, reactivated, landUnitID)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()
		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		local freq = 259500000
		if frequency then
			freq = math.floor(frequency*1000000)
		end

		local setfreq = {
			id = 'SetFrequency', 
			params = { 
				frequency = freq,
				modulation = 0
			} 
		}

		local distFromPoint = 10000
		local theta = math.random() * 2 * math.pi
  
		local dx = distFromPoint * math.cos(theta)
		local dy = distFromPoint * math.sin(theta)

		local pos1 = {
			x = point.x + dx,
			y = point.z + dy
		}

		local pos2 = {
			x = point.x - dx,
			y = point.z - dy
		}

		local orbit = { 
			id = 'Orbit', 
			params = { 
				pattern = AI.Task.OrbitPattern.RACE_TRACK,
				point = pos1,
   				point2 = pos2,
				altitude = alt
			}
		}

		local script = {
			id = "WrappedAction",
			params = {
				action = {
					id = "Script",
					params = 
					{
						command = "trigger.action.outTextForCoalition("..group:getCoalition()..", 'AWACS on station. "..(freq/1000000).." AM', 15)",
					}
				}
			}
		}


		local awacs = {
			id = 'ComboTask',
			params = {
				tasks = {
					{
						id = "WrappedAction",
						params = 
						{
							action = 
							{
								id = "EPLRS",
								params = {
									value = true,
									groupId = group:getID(),
								}
							}
						}
					},
					{
						id = 'AWACS', 
						params = { 
						}	
					}
				}
			}
		}

		local task = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {}
				}  
			}
		}

		if reactivated then
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = pos1.x,
				y = pos1.y,
				speed = 257,
				action = AI.Task.TurnMethod.FLY_OVER_POINT,
				alt = alt,
				alt_type = AI.Task.AltitudeType.BARO,
				task = awacs
			})
		else
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.TAKEOFF,
				x = startPos.x,
				y = startPos.z,
				speed = 0,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO,
				task = awacs
			})
		end
			
		table.insert(task.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = pos1.x,
			y = pos1.y,
			speed = 257,
			action = AI.Task.TurnMethod.FLY_OVER_POINT,
			alt = alt,
			alt_type = AI.Task.AltitudeType.BARO,
			task = {
				id = 'ComboTask',
				params = {
					tasks = {
						script
					}
				}
			}
		})

		table.insert(task.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = pos2.x,
			y = pos2.y,
			speed = 257,
			action = AI.Task.TurnMethod.FLY_OVER_POINT,
			alt = alt,
			alt_type = AI.Task.AltitudeType.BARO,
			task = {
				id = 'ComboTask',
				params = {
					tasks = {
						orbit
					}
				}
			}
		})

		if landUnitID then
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				linkUnit = landUnitID,
				helipadId = landUnitID,
				x = startPos.x,
				y = startPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		else
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				x = startPos.x,
				y = startPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		end
		
		group:getController():setTask(task)
		group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE)
		group:getController():setCommand(setfreq)
	end
	
	function TaskExtensions.executePatrolMission(group, point, altitude, range, reactivated, landUnitID)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		if reactivated then
			reactivated.currentPos = startPos
			startPos = reactivated.homePos
		end

		local rng = 25 * 1852
		if range then
			rng = range * 1852
		end
		
		local alt = 4572
		if altitude then
			alt = altitude/3.281
		end

		local search = { 
			id = 'EngageTargets',
			params = {
				maxDist = rng,
				targetTypes = { 'Planes', 'Helicopters' }
			} 
		}

		local distFromPoint = 10000
		local theta = math.random() * 2 * math.pi
  
		local dx = distFromPoint * math.cos(theta)
		local dy = distFromPoint * math.sin(theta)

		local p1 = {
			x = point.x + dx,
			y = point.z + dy
		}

		local p2 = {
			x = point.x - dx,
			y = point.z - dy
		}

		local orbit = {
			id = 'Orbit',
			params = {
				pattern = AI.Task.OrbitPattern.RACE_TRACK,
				point = p1,
				point2 = p2,
				speed = 154,
				altitude = alt
			}
		}

		local task = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {}
				}  
			}
		}

		if not reactivated then
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.TAKEOFF,
				x = startPos.x,
				y = startPos.z,
				speed = 0,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO,
				task = search
			})
		else
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = reactivated.currentPos.x,
				y = reactivated.currentPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = alt,
				alt_type = AI.Task.AltitudeType.BARO,
				task = search
			})
		end

		table.insert(task.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = p1.x,
			y = p1.y,
			speed = 257,
			action = AI.Task.TurnMethod.FLY_OVER_POINT,
			alt = alt,
			alt_type = AI.Task.AltitudeType.BARO
		})

		table.insert(task.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = p2.x,
			y = p2.y,
			speed = 257,
			action = AI.Task.TurnMethod.FLY_OVER_POINT,
			alt = alt,
			alt_type = AI.Task.AltitudeType.BARO,
			task = orbit
		})

		if landUnitID then
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				linkUnit = landUnitID,
				helipadId = landUnitID,
				x = startPos.x,
				y = startPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		else
			table.insert(task.params.route.points, {
				type= AI.Task.WaypointType.LAND,
				x = startPos.x,
				y = startPos.z,
				speed = 257,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = 0,
				alt_type = AI.Task.AltitudeType.RADIO
			})
		end
		
		group:getController():setTask(task)
		TaskExtensions.setDefaultAA(group)
	end

	function TaskExtensions.setDefaultAA(group)
		group:getController():setOption(AI.Option.Air.id.PROHIBIT_AG, true)
		group:getController():setOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, true)
		group:getController():setOption(AI.Option.Air.id.PROHIBIT_JETT, true)
		group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE)
		group:getController():setOption(AI.Option.Air.id.MISSILE_ATTACK, AI.Option.Air.val.MISSILE_ATTACK.MAX_RANGE)

		local weapons = 268402688  -- AnyMissile
		group:getController():setOption(AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, weapons)
	end

	function TaskExtensions.setDefaultAG(group)
		--group:getController():setOption(AI.Option.Air.id.PROHIBIT_AA, true)
		group:getController():setOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, true)
		group:getController():setOption(AI.Option.Air.id.PROHIBIT_JETT, true)
		group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE)

		local weapons = 2147485694 + 30720 + 4161536 -- AnyBomb + AnyRocket + AnyASM
		group:getController():setOption(AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, weapons)
	end

	function TaskExtensions.stopAndDisperse(group)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local pos = group:getUnit(1):getPoint()
		group:getController():setTask({
			id='Mission',
			params = {
				route = {
					points = {
						[1] = {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = pos.x,
							y = pos.z,
							speed = 1000,
							action = AI.Task.VehicleFormation.OFF_ROAD
						},
						[2] = {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = pos.x+math.random(25),
							y = pos.z+math.random(25),
							speed = 1000,
							action = AI.Task.VehicleFormation.DIAMOND
						},
					}
				}  
			}
        })
	end

	function TaskExtensions.moveOnRoadToPointAndAssault(group, point, targets, detour)
		if not group or not point then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		local srx, sry = land.getClosestPointOnRoads('roads', startPos.x, startPos.z)
		local erx, ery = land.getClosestPointOnRoads('roads', point.x, point.y)

		local mis = {
			id='Mission',
			params = {
				route = {
					points = {}
				}  
			}
		}

		if detour then
			local detourPoint = {x = startPos.x, y = startPos.z}

			local direction = {
				x = erx - startPos.x,
				y = ery - startPos.y
			}

			local magnitude = (direction.x^2 + direction.y^2) ^ 0.5
			if magnitude > 0.0 then
				direction.x = direction.x / magnitude
				direction.y = direction.y / magnitude

				local scale = math.random(250,500)
				direction.x = direction.x * scale
				direction.y = direction.y * scale

				detourPoint.x = detourPoint.x + direction.x
				detourPoint.y = detourPoint.y + direction.y
			else
				detourPoint.x = detourPoint.x + math.random(-500,500)
				detourPoint.y = detourPoint.y + math.random(-500,500)
			end
				
			table.insert(mis.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = detourPoint.x,
				y = detourPoint.y,
				speed = 1000,
				action = AI.Task.VehicleFormation.OFF_ROAD
			})

			srx, sry = land.getClosestPointOnRoads('roads', detourPoint.x, detourPoint.y)
		end

		
		table.insert(mis.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = srx,
			y = sry,
			speed = 1000,
			action = AI.Task.VehicleFormation.ON_ROAD
		})

		table.insert(mis.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = erx,
			y = ery,
			speed = 1000,
			action = AI.Task.VehicleFormation.ON_ROAD
		})

		table.insert(mis.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = point.x,
			y = point.y,
			speed = 1000,
			action = AI.Task.VehicleFormation.DIAMOND
		})
	

		for i,v in pairs(targets) do
			if v.type == 'defense' then
				local group = Group.getByName(v.name)
				if group then
					for i,v in ipairs(group:getUnits()) do
						local unpos = v:getPoint()
						local pnt = {x=unpos.x, y = unpos.z}
	
						table.insert(mis.params.route.points, {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = pnt.x,
							y = pnt.y,
							speed = 10,
							action = AI.Task.VehicleFormation.DIAMOND
						})
					end
				end
			end
		end
		
		group:getController():setTask(mis)
	end
	
	function TaskExtensions.moveOnRoadToPoint(group, point, detour) -- point = {x,y}
		if not group or not point then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()
		
		local srx, sry = land.getClosestPointOnRoads('roads', startPos.x, startPos.z)
		local erx, ery = land.getClosestPointOnRoads('roads', point.x, point.y)

		local mis = {
			id='Mission',
			params = {
				route = {
					points = {
					}
				}  
			}
		}

		if detour then
			local detourPoint = {x = startPos.x, y = startPos.z}

			local direction = {
				x = erx - startPos.x,
				y = ery - startPos.y
			}

			local magnitude = (direction.x^2 + direction.y^2) ^ 0.5
			if magnitude > 0.0 then
				direction.x = direction.x / magnitude
				direction.y = direction.y / magnitude

				local scale = math.random(250,1000)
				direction.x = direction.x * scale
				direction.y = direction.y * scale

				detourPoint.x = detourPoint.x + direction.x
				detourPoint.y = detourPoint.y + direction.y
			else
				detourPoint.x = detourPoint.x + math.random(-500,500)
				detourPoint.y = detourPoint.y + math.random(-500,500)
			end
				
			table.insert(mis.params.route.points, {
				type= AI.Task.WaypointType.TURNING_POINT,
				x = detourPoint.x,
				y = detourPoint.y,
				speed = 1000,
				action = AI.Task.VehicleFormation.OFF_ROAD
			})

			srx, sry = land.getClosestPointOnRoads('roads', detourPoint.x, detourPoint.y)
		end

		table.insert(mis.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = srx,
			y = sry,
			speed = 1000,
			action = AI.Task.VehicleFormation.ON_ROAD
		})

		table.insert(mis.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = erx,
			y = ery,
			speed = 1000,
			action = AI.Task.VehicleFormation.ON_ROAD
		})

		table.insert(mis.params.route.points, {
			type= AI.Task.WaypointType.TURNING_POINT,
			x = point.x,
			y = point.y,
			speed = 1000,
			action = AI.Task.VehicleFormation.OFF_ROAD
		})

		group:getController():setTask(mis)
	end
	
	function TaskExtensions.landAtPointFromAir(group, point, alt)
		if not group or not point then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		local atype = AI.Task.AltitudeType.RADIO
		if alt then
			atype = AI.Task.AltitudeType.BARO
		else
			alt = 500
		end

		local land = {
			id='Land',
			params = {
				point = point
			}
		}
		
		local mis = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {
						[1] = {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = startPos.x,
							y = startPos.z,
							speed = 500,
							action = AI.Task.TurnMethod.FIN_POINT,
							alt = alt,
							alt_type = atype
						},
						[2] = {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = point.x,
							y = point.y,
							speed = 257,
							action = AI.Task.TurnMethod.FIN_POINT,
							alt = alt,
							alt_type = atype, 
							task = land
						}
					}
				}  
			}
		}
		
		group:getController():setTask(mis)
	end

	function TaskExtensions.landAtPoint(group, point, alt, skiptakeoff) -- point = {x,y}
		if not group or not point then return end
		if not group:isExist() or group:getSize()==0 then return end
		local startPos = group:getUnit(1):getPoint()

		local atype = AI.Task.AltitudeType.RADIO
		if alt then
			atype = AI.Task.AltitudeType.BARO
		else
			alt = 500
		end
		
		local land = {
			id='Land',
			params = {
				point = point
			}
		}

		local mis = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {}
				}  
			}
		}

		if not skiptakeoff then
			table.insert(mis.params.route.points,{
				type = AI.Task.WaypointType.TAKEOFF,
				x = startPos.x,
				y = startPos.z,
				speed = 0,
				action = AI.Task.TurnMethod.FIN_POINT,
				alt = alt,
				alt_type = atype
			})
		end
		
		table.insert(mis.params.route.points,{
			type = AI.Task.WaypointType.TURNING_POINT,
			x = point.x,
			y = point.y,
			speed = 257,
			action = AI.Task.TurnMethod.FIN_POINT,
			alt = alt,
			alt_type = atype, 
			task = land
		})
		
		group:getController():setTask(mis)
	end

	function TaskExtensions.landAtAirfield(group, point) -- point = {x,y}
		if not group or not point then return end
		if not group:isExist() or group:getSize()==0 then return end
		
		local mis = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {
						[1] = {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = point.x,
							y = point.z,
							speed = 257,
							action = AI.Task.TurnMethod.FIN_POINT,
							alt = 4572,
							alt_type = AI.Task.AltitudeType.BARO
						},
						[2] = {
							type= AI.Task.WaypointType.LAND,
							x = point.x,
							y = point.z,
							speed = 257,
							action = AI.Task.TurnMethod.FIN_POINT,
							alt = 0,
							alt_type = AI.Task.AltitudeType.RADIO
						}
					}
				}  
			}
		}
		
		group:getController():setTask(mis)
	end

	function TaskExtensions.fireAtTargets(group, targets, amount)
		if not group then return end
		if not group:isExist() or group:getSize() == 0 then return end

		local units = {}
		for i,v in pairs(targets) do
			local g = Group.getByName(v.name)
			if g then
				for i2,v2 in ipairs(g:getUnits()) do
					table.insert(units, v2)
				end
			else
				local s = StaticObject.getByName(v.name)
				if s then
					table.insert(units, s)
				end
			end
		end
		
		if #units == 0 then
			return
		end
		
		local selected = {}
		for i=1,amount,1 do
			if #units == 0 then 
				break
			end
			
			local tgt = math.random(1,#units)
			
			table.insert(selected, units[tgt])
			table.remove(units, tgt)
		end
		
		while #selected < amount do
			local ind = math.random(1,#selected)
			table.insert(selected, selected[ind])
		end
		
		for i,v in ipairs(selected) do
			local unt = v
			if unt then
				local target = {}
				target.x = unt:getPosition().p.x
				target.y = unt:getPosition().p.z
				target.radius = 100
				target.expendQty = 1
				target.expendQtyEnabled = true
				local fire = {id = 'FireAtPoint', params = target}
				
				group:getController():pushTask(fire)
			end
		end
	end

	function TaskExtensions.carrierGoToPos(group, point)
		if not group or not point then return end
		if not group:isExist() or group:getSize()==0 then return end
		
		local mis = {
			id='Mission',
			params = {
				route = {
					airborne = true,
					points = {
						[1] = {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = point.x,
							y = point.z,
							speed = 50,
							action = AI.Task.TurnMethod.FIN_POINT
						}
					}
				}  
			}
		}
		
		group:getController():setTask(mis)
	end

	function TaskExtensions.stopCarrier(group)
		if not group then return end
		if not group:isExist() or group:getSize()==0 then return end
		local point = group:getUnit(1):getPoint()

		group:getController():setTask({
			id='Mission',
			params = {
				route = {
					airborne = false,
					points = {
						[1] = {
							type= AI.Task.WaypointType.TURNING_POINT,
							x = point.x,
							y = point.z,
							speed = 0,
							action = AI.Task.TurnMethod.FIN_POINT
						}
					}
				}  
			}
		})
	end

	function TaskExtensions.setupCarrier(unit, icls, acls, tacan, link4, radio)
		if not unit then return end
		if not unit:isExist() then return end

		local commands = {}
		if icls then
			table.insert(commands, { 
				id = 'ActivateICLS', 
				params = {
				  type = 131584,
				  channel = icls, 
				  unitId = unit:getID(), 
				  name = "ICLS "..icls, 
				} 
			})
		end

		if acls then
			table.insert(commands, { 
				id = 'ActivateACLS', 
				params = {
				  unitId = unit:getID(), 
				  name = "ACLS",
				}
			})
		end

		if tacan then
			table.insert(commands, { 
				id = 'ActivateBeacon', 
				params = { 
				  type = 4, 
				  system = 4, 
				  name = "TACAN "..tacan.channel, 
				  callsign = tacan.callsign, 
				  frequency = tacan.channel, 
				  channel = tacan.channel,
				  bearing = true,
				  modeChannel = "X"
				}
			})
		end

		if link4 then
			table.insert(commands, { 
				id = 'ActivateLink4', 
				params = {
				  unitId = unit:getID(),
				  frequency = link4, 
				  name = "Link4 "..link4, 
				}
			})
		end

		if radio then
			table.insert(commands, {
				id = "SetFrequency",
				params = {
				   power = 100,
				   modulation = 0,
				   frequency = radio,
				}
			})
		end

		for i,v in ipairs(commands) do
			unit:getController():setCommand(v)
		end

		unit:getGroup():getController():setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION, 30)
	end
end

-----------------[[ END OF TaskExtensions.lua ]]-----------------



-----------------[[ MarkerCommands.lua ]]-----------------

MarkerCommands = {}
do
	function MarkerCommands:new()
		local obj = {}
		obj.commands = {} --{command=string, action=function}
		
		setmetatable(obj, self)
		self.__index = self
		
		obj:start()
		
		DependencyManager.register("MarkerCommands", obj)
		return obj
	end
	
	function MarkerCommands:addCommand(command, action, hasParam, state)
		table.insert(self.commands, {command = command, action = action, hasParam = hasParam, state = state})
	end
	
	function MarkerCommands:start()
		local markEditedEvent = {}
		markEditedEvent.context = self
		function markEditedEvent:onEvent(event)
			if event.id == 26 and event.text and (event.coalition == 1 or event.coalition == 2) then -- mark changed
				local success = false
				env.info('MarkerCommands - input: '..event.text)
				
				for i,v in ipairs(self.context.commands) do
					if (not v.hasParam) and event.text == v.command then
						success = v.action(event, nil, v.state)
						break
					elseif v.hasParam and event.text:find('^'..v.command..':') then
						local param = event.text:gsub('^'..v.command..':', '')
						success = v.action(event, param, v.state)
						break
					end
				end
				
				if success then
					trigger.action.removeMark(event.idx)
				end
			end
		end
		
		world.addEventHandler(markEditedEvent)
	end
end


-----------------[[ END OF MarkerCommands.lua ]]-----------------



-----------------[[ ZoneCommand.lua ]]-----------------

ZoneCommand = {}
do
	ZoneCommand.currentZoneIndex = 1000
	ZoneCommand.allZones = {}
	ZoneCommand.buildSpeed = Config.buildSpeed
	ZoneCommand.supplyBuildSpeed = Config.supplyBuildSpeed
	ZoneCommand.missionValidChance = 0.9
	ZoneCommand.missionBuildSpeedReduction = Config.missionBuildSpeedReduction
	ZoneCommand.revealTime = 0
	ZoneCommand.staticRegistry = {}

	ZoneCommand.modes = {
		normal = 'normal',
		supply = 'supply',
		export = 'export'
	}
	
	ZoneCommand.productTypes = {
		upgrade = 'upgrade',
		mission = 'mission',
		defense = 'defense'
	}
	
	ZoneCommand.missionTypes = {
		supply_air = 'supply_air',
		supply_convoy = 'supply_convoy',
		cas = 'cas',
		cas_helo = 'cas_helo',
		strike = 'strike',
		patrol = 'patrol',
		sead = 'sead',
		assault = 'assault',
		bai = 'bai',
		supply_transfer = 'supply_transfer',
		awacs = 'awacs',
		tanker = 'tanker'
	}
	
	function ZoneCommand:new(zonename)
		local obj = {}
		obj.name = zonename
		obj.side = 0
		obj.resource = 0
		obj.resourceChange = 0
		obj.maxResource = 20000
		obj.spendTreshold = 5000
		obj.keepActive = false
		obj.boostScale = 1.0
		obj.extraBuildResources = 0
		obj.reservedMissions = {}
		obj.isHeloSpawn = false
		obj.isPlaneSpawn = false
		obj.spawnSurface = nil
		
		obj.zone = CustomZone:getByName(zonename)
		obj.products = {}
		obj.mode = 'normal' 
		--[[
			normal: buys whatever it can
			supply: buys only supply missions
			export: supply mode, but also sells all defense groups from the zone
		]]--
		obj.index = ZoneCommand.currentZoneIndex
		ZoneCommand.currentZoneIndex = ZoneCommand.currentZoneIndex + 1
		
		obj.built = {}
		obj.income = 0
		
		--group restrictions
		obj.spawns = {}
		for i,v in pairs(mist.DBs.groupsByName) do
			if v.units[1].skill == 'Client' then
				local zn = obj.zone
				local pos3d = {
					x = v.units[1].point.x,
					y = 0,
					z = v.units[1].point.y
				}
				
				if zn and zn:isInside(pos3d) then
					local coa = 0
					if v.coalition=='blue' then
						coa = 2
					elseif v.coalition=='red' then
						coa = 1
					end
					
					table.insert(obj.spawns, {name=i, side=coa})
				end
			end
		end
		
		--draw graphics
		local color = {0.7,0.7,0.7,0.3}
		if obj.side == 1 then
			color = {1,0,0,0.3}
		elseif obj.side == 2 then
			color = {0,0,1,0.3}
		end
		
		obj.zone:draw(obj.index, color, color)
		
		local point = obj.zone.point

		if obj.zone:isCircle() then
			point = {
				x = obj.zone.point.x,
				y = obj.zone.point.y,
				z = obj.zone.point.z + obj.zone.radius
			}
		elseif obj.zone:isQuad() then
			local largestZ = obj.zone.vertices[1].z
			local largestX = obj.zone.vertices[1].x
			for i=2,4,1 do
				if obj.zone.vertices[i].z > largestZ then
					largestZ = obj.zone.vertices[i].z
					largestX = obj.zone.vertices[i].x
				end
			end
			
			point = {
				x = largestX,
				y = obj.zone.point.y,
				z = largestZ
			}
		end
		
		--trigger.action.textToAll(1,1000+obj.index,point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '')
		--trigger.action.textToAll(2,2000+obj.index,point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '')
		trigger.action.textToAll(-1,2000+obj.index,point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '') --show blue to all
		setmetatable(obj, self)
		self.__index = self
		
		obj:refreshText()
		obj:start()
		obj:refreshSpawnBlocking()
		ZoneCommand.allZones[obj.name] = obj
		return obj
	end
	
	function ZoneCommand:refreshSpawnBlocking()
		for _,v in ipairs(self.spawns) do
			local isDifferentSide = v.side ~= self.side
			local noResources = self.resource < Config.zoneSpawnCost

			trigger.action.setUserFlag(v.name, isDifferentSide or noResources)
		end
	end
	
	function ZoneCommand.setNeighbours()
		local conManager = DependencyManager.get("ConnectionManager")
		for name,zone in pairs(ZoneCommand.allZones) do
			local neighbours = conManager:getConnectionsOfZone(name)
			zone.neighbours = {}
			for _,zname in ipairs(neighbours) do
				zone.neighbours[zname] = ZoneCommand.getZoneByName(zname)
			end
		end
	end
	
	function ZoneCommand.getZoneByName(name)
		if not name then return nil end
		return ZoneCommand.allZones[name]
	end
	
	function ZoneCommand.getAllZones()
		return ZoneCommand.allZones
	end
	
	function ZoneCommand.getZoneOfUnit(unitname)
		local un = Unit.getByName(unitname)
		
		if not un then 
			return nil
		end
		
		for i,v in pairs(ZoneCommand.allZones) do
			if Utils.isInZone(un, i) then
				return v
			end
		end
		
		return nil
	end
	
	function ZoneCommand.getZoneOfWeapon(weapon)
		if not weapon then 
			return nil
		end
		
		for i,v in pairs(ZoneCommand.allZones) do
			if Utils.isInZone(weapon, i) then
				return v
			end
		end
		
		return nil
	end

	function ZoneCommand.getClosestZoneToPoint(point, side)
		local minDist = 9999999
		local closest = nil
		for i,v in pairs(ZoneCommand.allZones) do
			if not side or side == v.side then
				local d = mist.utils.get2DDist(v.zone.point, point)
				if d < minDist then
					minDist = d
					closest = v
				end
			end
		end
		
		return closest, minDist
	end
	
	function ZoneCommand.getZoneOfPoint(point)
		for i,v in pairs(ZoneCommand.allZones) do
			local z = CustomZone:getByName(i)
			if z and z:isInside(point) then
				return v
			end
		end
		
		return nil
	end

	function ZoneCommand:boostProduction(amount)
		self.extraBuildResources = self.extraBuildResources + amount
		env.info('ZoneCommand:boostProduction - '..self.name..' production boosted by '..amount..' to a total of '..self.extraBuildResources)
	end

	function ZoneCommand:sabotage(explosionSize, sourcePoint)
		local minDist = 99999999
		local closest = nil
		for i,v in pairs(self.built) do
			if v.type == 'upgrade' then
				local st = StaticObject.getByName(v.name)
				if not st then st = Group.getByName(v.name) end
				local pos = st:getPoint()

				local d = mist.utils.get2DDist(pos, sourcePoint)
				if d < minDist then
					minDist = d;
					closest = pos
				end
			end
		end

		if closest then
			trigger.action.explosion(closest, explosionSize)
			env.info('ZoneCommand:sabotage - Structure has been sabotaged at '..self.name)
		end

		local damagedResources = math.random(2000,5000)
		self:removeResource(damagedResources)
		self:refreshText()
	end
	
	function ZoneCommand:refreshText()
		local build = ''
		if self.currentBuild then
			local job = ''
			local display = self.currentBuild.product.display
			if self.currentBuild.product.type == 'upgrade' then
				job = display
			elseif self.currentBuild.product.type == 'defense' then
				if self.currentBuild.isRepair then
					job = display..' (repair)'
				else
					job = display
				end
			elseif self.currentBuild.product.type == 'mission' then
				job = display
			end
			
			build = '\n['..job..' '..math.min(math.floor((self.currentBuild.progress/self.currentBuild.product.cost)*100),100)..'%]'
		end

		local mBuild = ''
		if self.currentMissionBuild then
			local job = ''
			local display = self.currentMissionBuild.product.display
			job = display
			
			mBuild = '\n['..job..' '..math.min(math.floor((self.currentMissionBuild.progress/self.currentMissionBuild.product.cost)*100),100)..'%]'
		end
		
		local status=''
		if self.side ~= 0 and self:criticalOnSupplies() then
			status = '(!)'
		end

		local color = {0.3,0.3,0.3,1}
		if self.side == 1 then
			color = {0.7,0,0,1}
		elseif self.side == 2 then
			color = {0,0,0.7,1}
		end

		--trigger.action.setMarkupColor(1000+self.index, color)
		trigger.action.setMarkupColor(2000+self.index, color)

		local label = '['..self.resource..'/'..self.maxResource..']'..status..build..mBuild

		if self.side == 1 then
			--trigger.action.setMarkupText(1000+self.index, self.name..label)

			if self.revealTime > 0 then
				trigger.action.setMarkupText(2000+self.index, self.name..label)
			else
				trigger.action.setMarkupText(2000+self.index, self.name)
			end
		elseif self.side == 2 then
			--if self.revealTime > 0 then
			--	trigger.action.setMarkupText(1000+self.index, self.name..label)
			--else
			--	trigger.action.setMarkupText(1000+self.index, self.name)
			--end
			trigger.action.setMarkupText(2000+self.index, self.name..label)
		elseif self.side == 0 then
			--trigger.action.setMarkupText(1000+self.index, ' '..self.name..' ')
			trigger.action.setMarkupText(2000+self.index, ' '..self.name..' ')
		end
	end
	
	function ZoneCommand:setSide(side)
		self.side = side
		self:refreshSpawnBlocking()

		if side == 0 then
			self.revealTime = 0
		end

		local color = {0.7,0.7,0.7,0.3}
		if self.side==1 then
			color = {1,0,0,0.3}
		elseif self.side==2 then
			color = {0,0,1,0.3}
		end
		
		trigger.action.setMarkupColorFill(self.index, color)
		trigger.action.setMarkupColor(self.index, color)
		trigger.action.setMarkupTypeLine(self.index, 1)

		if self.side == 2 and (self.isHeloSpawn or self.isPlaneSpawn) then
			trigger.action.setMarkupTypeLine(self.index, 2)
			trigger.action.setMarkupColor(self.index, {0,1,0,1})
		end

		self:refreshText()

		if self.airbaseName then
			local ab = Airbase.getByName(self.airbaseName)
			if ab then
				if ab:autoCaptureIsOn() then ab:autoCapture(false) end
				ab:setCoalition(self.side)
			else
				for i=1,10,1 do
					local ab = Airbase.getByName(self.airbaseName..'-'..i)
					if ab then
						if ab:autoCaptureIsOn() then ab:autoCapture(false) end
						ab:setCoalition(self.side)
					end
				end
			end
		end
	end
	
	function ZoneCommand:addResource(amount)
		self.resource = self.resource+amount
		self.resource = math.floor(math.min(self.resource, self.maxResource))
		self:refreshSpawnBlocking()
	end
	
	function ZoneCommand:removeResource(amount)
		self.resource = self.resource-amount
		self.resource = math.floor(math.max(self.resource, 0))
		self:refreshSpawnBlocking()
	end

	function ZoneCommand:reveal(time)
		local revtime = 30
		if time then
			revtime = time
		end

		self.revealTime = 60*revtime
		self:refreshText()
	end
	
	function ZoneCommand:needsSupplies(sendamount)
		return self.resource + sendamount<self.maxResource
	end
	
	function ZoneCommand:criticalOnSupplies()
		return self.resource<=self.spendTreshold
	end
	
	function ZoneCommand:capture(side, isSetup)
		if self.side == 0 then
			if not isSetup then
				local sidetxt = "Neutral"
				if side == 1 then
					sidetxt = "Red"
				elseif side == 2 then
					sidetxt = "Blue"
				end

				trigger.action.outText(self.name.." has been captured by "..sidetxt, 15)
			end
			self:setSide(side)
			local p = self.upgrades[side][1]
			self:instantBuild(p)
			if not isSetup then
				for i,v in pairs(p.products) do
					if v.cost < 0 then
						self:instantBuild(v)
					end
				end
			end
		end
	end
	
	function ZoneCommand:instantBuild(product)
		if product.type == 'defense' or product.type == 'upgrade' then
			env.info('ZoneCommand: instantBuild ['..product.name..']')
			if self.built[product.name] == nil then
				self.zone:spawnGroup(product, self.spawnSurface)
				self.built[product.name] = product
			end
		elseif product.type == 'mission' then
			self:reserveMission(product)
			timer.scheduleFunction(function (param, tme)
				param.context:unReserveMission(param.product)
				if param.context:isMissionValid(param.product) then
					param.context:activateMission(param.product)
				end
			end, {context = self, product = product}, timer.getTime()+math.random(3,10))
		end
	end
	
	function ZoneCommand:fullUpgrade(resourcePercentage)
		if not resourcePercentage then resourcePercentage = 0.25 end
		self.resource = math.floor(self.maxResource*resourcePercentage)
		self:fullBuild()
	end

	function ZoneCommand:fullBuild(useCost)
		if self.side ~= 1 and self.side ~= 2 then return end

		for i,v in ipairs(self.upgrades[self.side]) do
			if useCost then
				local cost = v.cost * useCost
				if self.resource >= cost then
					self:removeResource(cost)
				else
					break
				end
			end

			self:instantBuild(v)

			for i2,v2 in ipairs(v.products) do
				if (v2.type == 'defense' or v2.type=='upgrade') and v2.cost > 0 then
					if useCost then
						local cost = v2.cost * useCost
						if self.resource >= cost then
							self:removeResource(cost)
						else
							break
						end
					end

					self:instantBuild(v2)
				end
			end
		end
	end
	
	function ZoneCommand:start()
		timer.scheduleFunction(function(param, time)
			local self = param.context
			local initialRes = self.resource
			
			--generate income
			if self.side ~= 0 then
				self:addResource(self.income)
			end
			
			--untrack destroyed zone upgrades
			for i,v in pairs(self.built) do
				local u = Group.getByName(i)
				if u and u:getSize() == 0 then
					u:destroy()
					self.built[i] = nil
				end
				
				if not u then
					u = StaticObject.getByName(i)
					if u and u:getLife()<1 then
						u:destroy()
						self.built[i] = nil
					end
				end
				
				if not u then 
					self.built[i] = nil
				end
			end
			
			--upkeep costs for defenses
			for i,v in pairs(self.built) do
				if v.type == 'defense' and v.upkeep then
					v.strikes = v.strikes or 0
					if self.resource >= v.upkeep then
						self:removeResource(v.upkeep)
						v.strikes = 0
					else
						if v.strikes < 6 then
							v.strikes = v.strikes+1
						else
							local u = Group.getByName(i)
							if u then 
								v.strikes = nil
								u:destroy()
								self.built[i] = nil
							end
						end
					end
				elseif v.type == 'upgrade' and v.income then
					self:addResource(v.income)
				end
			end
			
			--check if zone should be reverted to neutral
			local hasUpgrade = false
			for i,v in pairs(self.built) do
				if v.type=='upgrade' then
					hasUpgrade = true
					break
				end
			end
			
			if not hasUpgrade and self.side ~= 0 then
				local sidetxt = "Neutral"
				if self.side == 1 then
					sidetxt = "Red"
				elseif self.side == 2 then
					sidetxt = "Blue"
				end

				trigger.action.outText(sidetxt.." has lost control of "..self.name, 15)

				self:setSide(0)
				self.mode = 'normal'
				self.currentBuild = nil
				self.currentMissionBuild = nil
			end
			
			--sell defenses if export mode
			if self.side ~= 0 and self.mode == 'export' then
				for i,v in pairs(self.built) do
					if v.type=='defense' then
						local g = Group.getByName(i)
						if g then g:destroy() end
						self:addResource(math.floor(v.cost/2))
						self.built[i] = nil
					end
				end
			end
			
			self:verifyBuildValid()
			self:chooseBuild()
			self:progressBuild()
			
			self.resourceChange = self.resource - initialRes
			self:refreshText()
			
			--use revealTime resource
			if self.revealTime > 0 then
				self.revealTime = math.max(0,self.revealTime-10)
			end

			return time+10
		end, {context = self}, timer.getTime()+1)
	end
	
	function ZoneCommand:verifyBuildValid()
		if self.currentBuild then
			if self.side == 0 then
				self.currentBuild = nil 
				env.info('ZoneCommand:verifyBuildValid - stopping build, zone is neutral')
			end

			if self.mode == 'export' or self.mode == 'supply' then 
				if not (self.currentBuild.product.type == ZoneCommand.productTypes.upgrade or 
					self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_air or 
					self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_convoy or
					self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_transfer) then 
					env.info('ZoneCommand:verifyBuildValid - stopping build, mode is '..self.mode..' but mission is not supply')
					self.currentBuild = nil 
				end
			end
			
			if self.currentBuild and (self.currentBuild.product.type == 'defense' or self.currentBuild.product.type == 'mission') then
				for i,v in ipairs(self.upgrades[self.currentBuild.side]) do
					for i2,v2 in ipairs(v.products) do
						if v2.name == self.currentBuild.product.name then
							local g = Group.getByName(v.name)
							if not g then g = StaticObject.getByName(v.name) end
							
							if not g then 
								env.info('ZoneCommand:verifyBuildValid - stopping build, required upgrade no longer exists')
								self.currentBuild = nil 
								break
							end
						end
					end
					
					if not self.currentBuild then
						break
					end
				end
			end
		end

		if self.currentMissionBuild then
			if self.side == 0 then
				self.currentMissionBuild = nil 
				env.info('ZoneCommand:verifyBuildValid - stopping mission build, zone is neutral')
			end

			if (self.mode == 'export' and not self.keepActive) or self.mode == 'supply' then 
				env.info('ZoneCommand:verifyBuildValid - stopping mission build, mode is '..self.mode..'')
				self.currentMissionBuild = nil
			end
			
			if self.currentMissionBuild and self.currentMissionBuild.product.type == 'mission' then
				for i,v in ipairs(self.upgrades[self.currentMissionBuild.side]) do
					for i2,v2 in ipairs(v.products) do
						if v2.name == self.currentMissionBuild.product.name then
							local g = Group.getByName(v.name)
							if not g then g = StaticObject.getByName(v.name) end
							
							if not g then 
								env.info('ZoneCommand:verifyBuildValid - stopping mission build, required upgrade no longer exists')
								self.currentMissionBuild = nil 
								break
							end
						end
					end
					
					if not self.currentMissionBuild then
						break
					end
				end
			end
		end
	end
	
	function ZoneCommand:chooseBuild()
		local treshhold = self.spendTreshold
		--local treshhold = 0
		if self.side ~= 0 and self.currentBuild == nil then
			local canAfford = {}
			for _,v in ipairs(self.upgrades[self.side]) do
				local u = Group.getByName(v.name)
				if not u then u = StaticObject.getByName(v.name) end
				
				if not u then
						table.insert(canAfford, {product = v, reason='upgrade'})
				elseif u ~= nil then
					for _,v2 in ipairs(v.products) do
						if v2.type == 'mission'  then 
							if self.resource > treshhold and
								(v2.missionType == ZoneCommand.missionTypes.supply_air or 
								v2.missionType == ZoneCommand.missionTypes.supply_convoy or 
								v2.missionType == ZoneCommand.missionTypes.supply_transfer) then
								if self:isMissionValid(v2) and math.random() < ZoneCommand.missionValidChance then
									table.insert(canAfford, {product = v2, reason='mission'})
									if v2.bias then
										for _=1,v2.bias,1 do
											table.insert(canAfford, {product = v2, reason='mission'})
										end
									end
								end
							end
						elseif v2.type=='defense' and self.mode ~='export' and self.mode ~='supply' and v2.cost > 0 then
							local g = Group.getByName(v2.name)
							if not g then
								table.insert(canAfford, {product = v2, reason='defense'})
							elseif g:getSize() < (g:getInitialSize()*math.random(40,100)/100) then
								table.insert(canAfford, {product = v2, reason='repair'})
							end
						end
					end
				end
			end
			
			if #canAfford > 0 then
				local choice = math.random(1, #canAfford)
				
				if canAfford[choice] then
					local p = canAfford[choice]
					if p.reason == 'repair' then
						self:queueBuild(p.product, self.side, true)
					else
						self:queueBuild(p.product, self.side)
					end
				end
			end
		end

		if self.side ~= 0 and self.currentMissionBuild == nil then
			local canMission = {}
			for _,v in ipairs(self.upgrades[self.side]) do
				local u = Group.getByName(v.name)
				if not u then u = StaticObject.getByName(v.name) end
				if u ~= nil then
					for _,v2 in ipairs(v.products) do
						if v2.type == 'mission' then 
							if v2.missionType ~= ZoneCommand.missionTypes.supply_air and 
								v2.missionType ~= ZoneCommand.missionTypes.supply_convoy and 
								v2.missionType ~= ZoneCommand.missionTypes.supply_transfer then
								if self:isMissionValid(v2) and math.random() < ZoneCommand.missionValidChance then
									table.insert(canMission, {product = v2, reason='mission'})
									if v2.bias then
										for _=1,v2.bias,1 do
											table.insert(canMission, {product = v2, reason='mission'})
										end
									end
								end
							end
						end
					end
				end
			end

			if #canMission > 0 then
				local choice = math.random(1, #canMission)
				
				if canMission[choice] then
					local p = canMission[choice]
					self:queueBuild(p.product, self.side)
				end
			end
		end
	end
	
	function ZoneCommand:progressBuild()
		if self.currentBuild and self.currentBuild.side ~= self.side then
			env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, zone changed owner')
			self.currentBuild = nil
		end

		if self.currentMissionBuild and self.currentMissionBuild.side ~= self.side then
			env.info('ZoneCommand:progressBuild '..self.name..' - stopping mission build, zone changed owner')
			self.currentMissionBuild = nil
		end
		
		if self.currentBuild then
			if self.currentBuild.product.type == 'mission' and not self:isMissionValid(self.currentBuild.product) then
				env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, mission no longer valid')
				self.currentBuild = nil
			else
				local cost = self.currentBuild.product.cost
				if self.currentBuild.isRepair then
					cost = math.floor(self.currentBuild.product.cost/2)
				end
				
				if self.currentBuild.progress < cost then
					if self.currentBuild.isRepair and not Group.getByName(self.currentBuild.product.name) then
						env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, group to repair no longer exists')
						self.currentBuild = nil
					else
						if self.currentBuild.isRepair then
							local gr = Group.getByName(self.currentBuild.product.name)
							if gr and self.currentBuild.unitcount and gr:getSize() < self.currentBuild.unitcount then
								env.info('ZoneCommand:progressBuild '..self.name..' - restarting build, group to repair has casualties')
								self.currentBuild.unitcount = gr:getSize()
								self:addResource(self.currentBuild.progress)
								self.currentBuild.progress = 0
							end
						end

						local step = math.floor(ZoneCommand.buildSpeed * self.boostScale)
						if self.currentBuild.product.type == ZoneCommand.productTypes.mission then
							if self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_air or
								self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_convoy or
								self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_transfer then
								step = math.floor(ZoneCommand.supplyBuildSpeed * self.boostScale)

								if self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_transfer then
									step = math.floor(step*2)
								end
							end
						end
						
						if step > self.resource then step = 1 end
						if step <= self.resource then
							self:removeResource(step)
							self.currentBuild.progress = self.currentBuild.progress + step

							if self.extraBuildResources > 0 then
								local extrastep = step
								if self.extraBuildResources < extrastep then
									extrastep = self.extraBuildResources
								end

								self.extraBuildResources = math.max(self.extraBuildResources - extrastep, 0)
								self.currentBuild.progress = self.currentBuild.progress + extrastep
								
								env.info('ZoneCommand:progressBuild - '..self.name..' consumed '..extrastep..' extra resources, remaining '..self.extraBuildResources)
							end
						end
					end
				else
					if self.currentBuild.product.type == 'mission' then
						if self:isMissionValid(self.currentBuild.product) then
							self:activateMission(self.currentBuild.product)
						else
							self:addResource(self.currentBuild.product.cost)
						end
					elseif self.currentBuild.product.type == 'defense' or self.currentBuild.product.type=='upgrade' then
						if self.currentBuild.isRepair then
							if Group.getByName(self.currentBuild.product.name) then
								self.zone:spawnGroup(self.currentBuild.product, self.spawnSurface)
							end
						else
							self.zone:spawnGroup(self.currentBuild.product, self.spawnSurface)
						end
						
						self.built[self.currentBuild.product.name] = self.currentBuild.product
					end
					
					self.currentBuild = nil
				end
			end
		end

		if self.currentMissionBuild then
			if self.currentMissionBuild.product.type == 'mission' and not self:isMissionValid(self.currentMissionBuild.product) then
				env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, mission no longer valid')
				self.currentMissionBuild = nil
			else
				local cost = self.currentMissionBuild.product.cost
				
				if self.currentMissionBuild.progress < cost then
					local step = math.floor(ZoneCommand.buildSpeed * self.boostScale)

					if step > self.resource then step = 1 end

					local progress = step*self.missionBuildSpeedReduction
					local reducedCost = math.max(1, math.floor(progress))
					if reducedCost <= self.resource then
						self:removeResource(reducedCost)
						self.currentMissionBuild.progress = self.currentMissionBuild.progress + progress
					end
				else
					if self:isMissionValid(self.currentMissionBuild.product) then
						self:activateMission(self.currentMissionBuild.product)
					else
						self:addResource(self.currentMissionBuild.product.cost)
					end
					
					self.currentMissionBuild = nil
				end
			end
		end
	end
	
	function ZoneCommand:queueBuild(product, side, isRepair, progress)
		if product.type ~= ZoneCommand.productTypes.mission or 
			(product.missionType == ZoneCommand.missionTypes.supply_air or
			product.missionType == ZoneCommand.missionTypes.supply_convoy or
			product.missionType == ZoneCommand.missionTypes.supply_transfer) then
				
			local unitcount = nil
			if isRepair then
				local g = Group.getByName(product.name)
				if g then
					unitcount = g:getSize()
					env.info('ZoneCommand:queueBuild - '..self.name..' '..product.name..' has '..unitcount..' units')
				end
			end
			
			self.currentBuild = { product = product, progress = (progress or 0), side = side, isRepair = isRepair, unitcount = unitcount}
			env.info('ZoneCommand:queueBuild - '..self.name..' chose '..product.name..'('..product.display..') as its build')
		else
			self.currentMissionBuild = { product = product, progress = (progress or 0), side = side}
			env.info('ZoneCommand:queueBuild - '..self.name..' chose '..product.name..'('..product.display..') as its mission build')
		end
	end

	function ZoneCommand:reserveMission(product)
		self.reservedMissions[product.name] = product
	end

	function ZoneCommand:unReserveMission(product)
		self.reservedMissions[product.name] = nil
	end

	function ZoneCommand:isMissionValid(product)
		if Group.getByName(product.name) then return false end

		if self.reservedMissions[product.name] then
			return false
		end

		if product.missionType == ZoneCommand.missionTypes.supply_convoy then
			if self.distToFront == nil then return false end

			for _,tgt in pairs(self.neighbours) do
				if self:isSupplyMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.supply_transfer then
			if self.distToFront == nil then return false end 
			for _,tgt in pairs(self.neighbours) do
				if self:isSupplyTransferMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.supply_air then
			if self.distToFront == nil then return false end

			for _,tgt in pairs(self.neighbours) do
				if self:isSupplyMissionValid(product, tgt) then 
					return true 
				else
					for _,subtgt in pairs(tgt.neighbours) do
						if subtgt.name ~= self.name and self:isSupplyMissionValid(product, subtgt) then
							local dist = mist.utils.get2DDist(self.zone.point, subtgt.zone.point)
							if dist < 50000 then
								return true
							end
						end
					end
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.assault then
			if self.mode ~= ZoneCommand.modes.normal then return false end
			for _,tgt in pairs(self.neighbours) do
				if self:isAssaultMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.cas then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end

			for _,tgt in pairs(ZoneCommand.getAllZones()) do
				if self:isCasMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.cas_helo then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end

			for _,tgt in pairs(self.neighbours) do
				if self:isCasMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.strike then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end

			for _,tgt in pairs(ZoneCommand.getAllZones()) do
				if self:isStrikeMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.sead then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end

			for _,tgt in pairs(ZoneCommand.getAllZones()) do
				if self:isSeadMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.patrol then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end

			for _,tgt in pairs(ZoneCommand.getAllZones()) do
				if self:isPatrolMissionValid(product, tgt) then 
					return true 
				end
			end
		elseif product.missionType == ZoneCommand.missionTypes.bai then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end

			for _,tgt in pairs(DependencyManager.get("GroupMonitor").groups) do
				if self:isBaiMissionValid(product, tgt) then 
					return true
				end	
			end
		elseif product.missionType == ZoneCommand.missionTypes.awacs then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end
			for _,tgt in pairs(ZoneCommand.getAllZones()) do
				if self:isAwacsMissionValid(product, tgt) then 
					return true
				end	
			end
		elseif product.missionType == ZoneCommand.missionTypes.tanker then
			if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end
			if not self.distToFront or self.distToFront == 0 then return false end
			for _,tgt in pairs(ZoneCommand.getAllZones()) do
				if self:isTankerMissionValid(product, tgt) then 
					return true
				end	
			end
		end
	end

	function ZoneCommand:activateMission(product)
		if product.missionType == ZoneCommand.missionTypes.supply_convoy then
			self:activateSupplyConvoyMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.assault then
			self:activateAssaultMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.supply_air then
			self:activateAirSupplyMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.supply_transfer then
			self:activateSupplyTransferMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.cas then
			self:activateCasMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.cas_helo then
			self:activateCasMission(product, true)
		elseif product.missionType == ZoneCommand.missionTypes.strike then
			self:activateStrikeMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.sead then
			self:activateSeadMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.patrol then
			self:activatePatrolMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.bai then
			self:activateBaiMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.awacs then
			self:activateAwacsMission(product)
		elseif product.missionType == ZoneCommand.missionTypes.tanker then
			self:activateTankerMission(product)
		end

		env.info('ZoneCommand:activateMission - '..self.name..' activating mission '..product.name..'('..product.display..')')
	end

	function ZoneCommand:reActivateMission(savedData)
		local product = self:getProductByName(savedData.productName)

		if product.missionType == ZoneCommand.missionTypes.supply_convoy then
			self:reActivateSupplyConvoyMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.assault then
			self:reActivateAssaultMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.supply_air then
			self:reActivateAirSupplyMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.supply_transfer then
			self:reActivateSupplyTransferMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.cas then
			self:reActivateCasMission(product, nil, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.cas_helo then
			self:reActivateCasMission(product, true, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.strike then
			self:reActivateStrikeMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.sead then
			self:reActivateSeadMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.patrol then
			self:reActivatePatrolMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.bai then
			self:reActivateBaiMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.awacs then
			self:reActivateAwacsMission(product, savedData)
		elseif product.missionType == ZoneCommand.missionTypes.tanker then
			self:reActivateTankerMission(product, savedData)
		end

		env.info('ZoneCommand:reActivateMission - '..self.name..' reactivating mission '..product.name..'('..product.display..')')
	end

	local function getDefaultPos(savedData, isAir)
		local action = 'Off Road'
		local speed = 0
		if isAir then
			action = 'Turning Point'
			speed = 250
		end

		local vars = {
			groupName = savedData.productName,
			point = savedData.position,
			action = 'respawn',
			heading = savedData.heading,
			initTasks = false,
			route = { 
				[1] = {
					alt = savedData.position.y,
					type = 'Turning Point',
					action = action,
					alt_type = 'BARO',
					x = savedData.position.x,
					y = savedData.position.z,
					speed = speed
				}
			}
		}

		return vars
	end

	local function teleportToPos(groupName, pos)
		if pos.y == nil then
			pos.y = land.getHeight({ x = pos.x, y = pos.z })
		end

		local vars = {
			groupName = groupName,
			point = pos,
			action = 'respawn',
			initTasks = false
		}

		mist.teleportToPoint(vars)
	end

	function ZoneCommand:reActivateSupplyConvoyMission(product, savedData)
		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)

		local supplyPoint = trigger.misc.getZone(zone.name..'-sp')
		if not supplyPoint then
			supplyPoint = trigger.misc.getZone(zone.name)
		end
		if supplyPoint then 
			mist.teleportToPoint(getDefaultPos(savedData, false))
			DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData)

			product.lastMission = {zoneName = zone.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.name)
				TaskExtensions.moveOnRoadToPoint(gr, param.point)
			end, {name=product.name, point={ x=supplyPoint.point.x, y = supplyPoint.point.z}}, timer.getTime()+1)
		end
	end
	
	function ZoneCommand:reActivateAssaultMission(product, savedData)
		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)

		local supplyPoint = trigger.misc.getZone(zone.name..'-sp')
		if not supplyPoint then
			supplyPoint = trigger.misc.getZone(zone.name)
		end
		if supplyPoint then 
			mist.teleportToPoint(getDefaultPos(savedData, false))
			DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData)

			local tgtPoint = trigger.misc.getZone(zone.name)

			if tgtPoint then 
				product.lastMission = {zoneName = zone.name}
				timer.scheduleFunction(function(param)
					local gr = Group.getByName(param.name)
					TaskExtensions.moveOnRoadToPointAndAssault(gr, param.point, param.targets)
				end, {name=product.name, point={ x=tgtPoint.point.x, y = tgtPoint.point.z}, targets=zone.built}, timer.getTime()+1)
			end
		end
	end
	
	function ZoneCommand:reActivateAirSupplyMission(product, savedData)
		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)

		mist.teleportToPoint(getDefaultPos(savedData, true))
		DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData)

		local supplyPoint = trigger.misc.getZone(zone.name..'-hsp')
		if not supplyPoint then
			supplyPoint = trigger.misc.getZone(zone.name)
		end

		if supplyPoint then 
			product.lastMission = {zoneName = zone.name}
			local alt = DependencyManager.get("ConnectionManager"):getHeliAlt(self.name, zone.name)
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.name)
				TaskExtensions.landAtPoint(gr, param.point, param.alt)
			end, {name=product.name, point={ x=supplyPoint.point.x, y = supplyPoint.point.z}, alt = alt}, timer.getTime()+1)
		end
	end
	
	function ZoneCommand:reActivateSupplyTransferMission(product, savedData)
		-- not needed
	end
	
	function ZoneCommand:reActivateCasMission(product, isHelo, savedData)
		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)

		mist.teleportToPoint(getDefaultPos(savedData, true))
		DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData)

		local homePos = trigger.misc.getZone(savedData.homeName).point

		if zone then 
			product.lastMission = {zoneName = zone.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				if param.helo then
					TaskExtensions.executeHeloCasMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos})
				else
					TaskExtensions.executeCasMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos})
				end
			end, {prod=product, targets=zone.built, helo = isHelo, homePos = homePos}, timer.getTime()+1)
		end
	end
	
	function ZoneCommand:reActivateStrikeMission(product, savedData)
		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)

		mist.teleportToPoint(getDefaultPos(savedData, true))

		DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData)

		local homePos = trigger.misc.getZone(savedData.homeName).point

		if zone then 
			product.lastMission = {zoneName = zone.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				TaskExtensions.executeStrikeMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos})
			end, {prod=product, targets=zone.built, homePos = homePos}, timer.getTime()+1)
		end
	end
	
	function ZoneCommand:reActivateSeadMission(product, savedData)
		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)

		mist.teleportToPoint(getDefaultPos(savedData, true))
		DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData)

		local homePos = trigger.misc.getZone(savedData.homeName).point

		if zone then 
			product.lastMission = {zoneName = zone.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				TaskExtensions.executeSeadMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos})
			end, {prod=product, targets=zone.built, homePos = homePos}, timer.getTime()+1)
		end
	end
	
	function ZoneCommand:reActivatePatrolMission(product, savedData)

		local zn1 = ZoneCommand.getZoneByName(savedData.lastMission.zone1name)

		mist.teleportToPoint(getDefaultPos(savedData, true))
		DependencyManager.get("GroupMonitor"):registerGroup(product, zn1, self, savedData)

		local homePos = trigger.misc.getZone(savedData.homeName).point

		if zn1 then 
			product.lastMission = {zone1name = zn1.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)

				local point = trigger.misc.getZone(param.zone1.name).point

				TaskExtensions.executePatrolMission(gr, point, param.prod.altitude, param.prod.range, {homePos = param.homePos})
			end, {prod=product, zone1 = zn1, homePos = homePos}, timer.getTime()+1)
		end
	end
	
	function ZoneCommand:reActivateBaiMission(product, savedData)
		local targets = {}
		local hasTarget = false
		for _,tgt in pairs(DependencyManager.get("GroupMonitor").groups) do
			if self:isBaiMissionValid(product, tgt) then 
				targets[tgt.product.name] = tgt.product
				hasTarget = true
			end	
		end
		
		local homePos = trigger.misc.getZone(savedData.homeName).point

		if hasTarget then 
			mist.teleportToPoint(getDefaultPos(savedData, true))
			DependencyManager.get("GroupMonitor"):registerGroup(product, nil, self, savedData)

			product.lastMission = { active = true }
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				TaskExtensions.executeBaiMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = param.homePos})
			end, {prod=product, targets=targets, homePos = homePos}, timer.getTime()+1)
		end
	end
	
	function ZoneCommand:reActivateAwacsMission(product, savedData)
		
		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)
		local homePos = trigger.misc.getZone(savedData.homeName).point

		mist.teleportToPoint(getDefaultPos(savedData, true))
		DependencyManager.get("GroupMonitor"):registerGroup(product, nil, self, savedData)
		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.prod.name)
			if gr then
				local un = gr:getUnit(1)
				if un then 
					local callsign = un:getCallsign()
					RadioFrequencyTracker.registerRadio(param.prod.name, '[AWACS] '..callsign, param.prod.freq..' AM')
				end

				local point = trigger.misc.getZone(param.target.name).point
				product.lastMission = { zoneName = param.target.name }
				TaskExtensions.executeAwacsMission(gr, point, param.prod.altitude, param.prod.freq, {homePos = param.homePos})
			end
		end, {prod=product, target=zone, homePos = homePos}, timer.getTime()+1)
	end
	
	function ZoneCommand:reActivateTankerMission(product, savedData)

		local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName)

		local homePos = trigger.misc.getZone(savedData.homeName).point
		mist.teleportToPoint(getDefaultPos(savedData, true))
		DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData)
		
		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.prod.name)
			if gr then
				local un = gr:getUnit(1)
				if un then 
					local callsign = un:getCallsign()
					RadioFrequencyTracker.registerRadio(param.prod.name, '[Tanker('..param.prod.variant..')] '..callsign, param.prod.freq..' AM | TCN '..param.prod.tacan..'X')
				end

				local point = trigger.misc.getZone(param.target.name).point
				product.lastMission = { zoneName = param.target.name }
				TaskExtensions.executeTankerMission(gr, point, param.prod.altitude, param.prod.freq, param.prod.tacan, {homePos = param.homePos})
			end
		end, {prod=product, target=zone, homePos = homePos}, timer.getTime()+1)
	end

	function ZoneCommand:isBaiMissionValid(product, tgtgroup)
		if product.side == tgtgroup.product.side then return false end
		if tgtgroup.product.type ~= ZoneCommand.productTypes.mission then return false end
		if tgtgroup.product.missionType == ZoneCommand.missionTypes.assault then return true end
		if tgtgroup.product.missionType == ZoneCommand.missionTypes.supply_convoy then return true end
	end

	function ZoneCommand:activateBaiMission(product)
		--{name = product.name, lastStateTime = timer.getAbsTime(), product = product, target = target}
		local targets = {}
		local hasTarget = false
		for _,tgt in pairs(DependencyManager.get("GroupMonitor").groups) do
			if self:isBaiMissionValid(product, tgt) then 
				targets[tgt.product.name] = tgt.product
				hasTarget = true
			end	
		end

		if hasTarget then 
			local og = Utils.getOriginalGroup(product.name)
			if og then
				teleportToPos(product.name, {x=og.x, z=og.y})
				env.info("ZoneCommand - activateBaiMission teleporting to OG pos")
			else
				mist.respawnGroup(product.name, true)
				env.info("ZoneCommand - activateBaiMission fallback to respawnGroup")
			end

			DependencyManager.get("GroupMonitor"):registerGroup(product, nil, self)

			product.lastMission = { active = true }
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				TaskExtensions.executeBaiMission(gr, param.targets, param.prod.expend, param.prod.altitude)
			end, {prod=product, targets=targets}, timer.getTime()+1)

			env.info("ZoneCommand - "..product.name.." targeting convoys")
		end
	end

	local function prioritizeSupplyTargets(a,b)
		--if a:criticalOnSupplies() and not b:criticalOnSupplies() then return true end
		--if b:criticalOnSupplies() and not a:criticalOnSupplies() then return false end

		if a.distToFront~=nil and b.distToFront == nil then
			return true
		elseif a.distToFront == nil and b.distToFront ~= nil then
			return false
		elseif a.distToFront == b.distToFront then
			return a.resource < b.resource
		else
			return a.distToFront<b.distToFront
		end
	end

	function ZoneCommand:activateSupplyTransferMission(product)
		local tgtzones = {}
		for _,v in pairs(self.neighbours) do
			if (v.side == 0 or v.side==product.side) then
				table.insert(tgtzones, v)
			end
		end
		
		if #tgtzones == 0 then 
			env.info('ZoneCommand:activateSupplyTransferMission - '..self.name..' no valid tgtzones')
			return 
		end

		table.sort(tgtzones, prioritizeSupplyTargets)

		for i,v in ipairs(tgtzones) do
			if self:isSupplyTransferMissionValid(product, v) then
				-- virtual resourse transfer to save some performance
				v:addResource(product.cost)
				env.info('ZoneCommand:activateSupplyTransferMission - '..self.name..' transfered '..product.cost..' to '..v.name)
				break
			end
		end
	end

	function ZoneCommand:activateAirSupplyMission(product) 
		local tgtzones = {}
		for _,v in pairs(self.neighbours) do
			if (v.side == 0 or v.side==product.side) then
				table.insert(tgtzones, v)

				for _,v2 in pairs(v.neighbours) do
					if v2.name~=self.name and (v2.side == 0 or v2.side==product.side) then
						local dist = mist.utils.get2DDist(self.zone.point, v2.zone.point)
						if dist < 50000 then
							table.insert(tgtzones, v2)
						end
					end
				end
			end
		end
		
		if #tgtzones == 0 then 
			env.info('ZoneCommand:activateAirSupplyMission - '..self.name..' no valid tgtzones')
			return 
		end

		table.sort(tgtzones, prioritizeSupplyTargets)
		local viablezones = {}
		for _,v in ipairs(tgtzones) do
			viablezones[v.name] = v
		end

		if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then
			local prioZone = BattlefieldManager.priorityZones[self.side]
			if prioZone.side == 0 and viablezones[prioZone.name] and self:isSupplyMissionValid(product, prioZone) then
				tgtzones = { prioZone }
			end
		end

		for i,v in ipairs(tgtzones) do
			if self:isSupplyMissionValid(product, v) then

				local og = Utils.getOriginalGroup(product.name)
				if og then
					teleportToPos(product.name, {x=og.x, z=og.y})
					env.info("ZoneCommand - activateAirSupplyMission teleporting to OG pos")
				else
					mist.respawnGroup(product.name, true)
					env.info("ZoneCommand - activateAirSupplyMission fallback to respawnGroup")
				end

				DependencyManager.get("GroupMonitor"):registerGroup(product, v, self)

				local supplyPoint = trigger.misc.getZone(v.name..'-hsp')
				if not supplyPoint then
					supplyPoint = trigger.misc.getZone(v.name)
				end

				if supplyPoint then 
					product.lastMission = {zoneName = v.name}

					local alt = DependencyManager.get("ConnectionManager"):getHeliAlt(self.name, v.name)
					timer.scheduleFunction(function(param)
						local gr = Group.getByName(param.name)
						TaskExtensions.landAtPoint(gr, param.point, param.alt )
					end, {name=product.name, point={ x=supplyPoint.point.x, y = supplyPoint.point.z}, alt = alt}, timer.getTime()+1)

					env.info("ZoneCommand - "..product.name.." targeting "..v.name)
				end

				break
			end
		end
	end

	function ZoneCommand:isSupplyTransferMissionValid(product, target)
		if target.side == 0 then
			return false
		end

		if target.side == self.side then 
			if self.distToFront <= 1 or target.distToFront <= 1 then return false end -- skip transfer missions if close to front

			if not target.distToFront or not self.distToFront then
				return false
			end
			
			if target:needsSupplies(product.cost) and target.distToFront < self.distToFront then
				return true
			end
			
			if target:criticalOnSupplies() and self.distToFront<=target.distToFront then
				return true
			end

			if target:criticalOnSupplies() and target.mode == 'normal' then 
				return true
			end
		end
	end

	function ZoneCommand:isSupplyMissionValid(product, target)
		if product.missionType == ZoneCommand.missionTypes.supply_convoy then
			if DependencyManager.get("ConnectionManager"):isRoadBlocked(self.name, target.name) then
				return false
			end
		end
		
		if not self.distToFront then return false end
		if not target.distToFront then return false end
		
		if target.side == 0 then
			return true
		end

		if target.side == self.side then 
			if self.distToFront > 1 and target.distToFront > 1 then return false end -- skip regular missions if not close to front

			if self.mode == 'normal' and self.distToFront == 0  and target.distToFront == 0 then
				return target:needsSupplies(product.cost*0.5)
			end
			
			if target:needsSupplies(product.cost*0.5) and target.distToFront < self.distToFront then
				return true
			elseif target:criticalOnSupplies() and self.distToFront>=target.distToFront then
				return true
			end

			if target.mode == 'normal' and target:needsSupplies(product.cost*0.5) then 
				return true
			end
		end
	end

	function ZoneCommand:activateSupplyConvoyMission(product)
		local tgtzones = {}
		for _,v in pairs(self.neighbours) do
			if (v.side == 0 or v.side==product.side) then
				table.insert(tgtzones, v)
			end
		end
		
		if #tgtzones == 0 then 
			env.info('ZoneCommand:activateSupplyConvoyMission - '..self.name..' no valid tgtzones')
			return 
		end

		table.sort(tgtzones, prioritizeSupplyTargets)

		if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then
			local prioZone = BattlefieldManager.priorityZones[self.side]
			if prioZone.side == 0 and self.neighbours[prioZone.name] and self:isSupplyMissionValid(product, prioZone) then
				tgtzones = { prioZone }
			end
		end

		for i,v in ipairs(tgtzones) do
			if self:isSupplyMissionValid(product, v) then

				local supplyPoint = trigger.misc.getZone(v.name..'-sp')
				if not supplyPoint then
					supplyPoint = trigger.misc.getZone(v.name)
				end
				
				if supplyPoint then 

					local og = Utils.getOriginalGroup(product.name)
					if og then
						teleportToPos(product.name, {x=og.x, z=og.y})
						env.info("ZoneCommand - activateSupplyConvoyMission teleporting to OG pos")
					else
						mist.respawnGroup(product.name, true)
						env.info("ZoneCommand - activateSupplyConvoyMission fallback to respawnGroup")
					end

					DependencyManager.get("GroupMonitor"):registerGroup(product, v, self)

					product.lastMission = {zoneName = v.name}
					timer.scheduleFunction(function(param)
						local gr = Group.getByName(param.name)
						TaskExtensions.moveOnRoadToPoint(gr, param.point)
					end, {name=product.name, point={ x=supplyPoint.point.x, y = supplyPoint.point.z}}, timer.getTime()+1)
					
					env.info("ZoneCommand - "..product.name.." targeting "..v.name)
				end

				break
			end
		end
	end

	function ZoneCommand:isAssaultMissionValid(product, target)

		if product.missionType == ZoneCommand.missionTypes.assault then
			if DependencyManager.get("ConnectionManager"):isRoadBlocked(self.name, target.name) then
				return false
			end
		end

		if target.side ~= product.side and target.side ~= 0 then
			return true
		end
	end

	function ZoneCommand:activateAssaultMission(product)
		local tgtzones = {}
		for _,v in pairs(self.neighbours) do
			table.insert(tgtzones, {zone = v, rank = math.random()})
		end

		table.sort(tgtzones, function(a,b) return a.rank < b.rank end)

		local sorted = {}
		for i,v in ipairs(tgtzones) do
			table.insert(sorted, v.zone)
		end
		tgtzones = sorted

		if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then
			local prioZone = BattlefieldManager.priorityZones[self.side]
			if self.neighbours[prioZone.name] and self:isAssaultMissionValid(product, prioZone) then
				tgtzones = { prioZone }
			end
		end

		for i,v in ipairs(tgtzones) do
			if self:isAssaultMissionValid(product, v) then

				local og = Utils.getOriginalGroup(product.name)
				if og then
					teleportToPos(product.name, {x=og.x, z=og.y})
					env.info("ZoneCommand - activateAssaultMission teleporting to OG pos")
				else
					mist.respawnGroup(product.name, true)
					env.info("ZoneCommand - activateAssaultMission fallback to respawnGroup")
				end

				DependencyManager.get("GroupMonitor"):registerGroup(product, v, self)

				local tgtPoint = trigger.misc.getZone(v.name)

				if tgtPoint then 
					product.lastMission = {zoneName = v.name}
					timer.scheduleFunction(function(param)
						local gr = Group.getByName(param.name)
						TaskExtensions.moveOnRoadToPointAndAssault(gr, param.point, param.targets)
					end, {name=product.name, point={ x=tgtPoint.point.x, y = tgtPoint.point.z}, targets=v.built}, timer.getTime()+1)

					env.info("ZoneCommand - "..product.name.." targeting "..v.name)
				end

				break
			end
		end
	end
	
	function ZoneCommand:isAwacsMissionValid(product, target)
		if target.side ~= product.side then return false end
		if target.name == self.name then return false end
		if not target.distToFront or target.distToFront ~= 4 then return false end
		
		return true
	end

	function ZoneCommand:activateAwacsMission(product)
		local tgtzones = {}
		for _,v in pairs(ZoneCommand.getAllZones()) do
			if self:isAwacsMissionValid(product, v) then
				table.insert(tgtzones, v)
			end
		end

		local choice1 = math.random(1,#tgtzones)
		local zn = tgtzones[choice1]

		local og = Utils.getOriginalGroup(product.name)
		if og then
			teleportToPos(product.name, {x=og.x, z=og.y})
			env.info("ZoneCommand - activateAwacsMission teleporting to OG pos")
		else
			mist.respawnGroup(product.name, true)
			env.info("ZoneCommand - activateAwacsMission fallback to respawnGroup")
		end

		DependencyManager.get("GroupMonitor"):registerGroup(product, zn, self)

		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.prod.name)
			if gr then
				local un = gr:getUnit(1)
				if un then 
					local callsign = un:getCallsign()
					RadioFrequencyTracker.registerRadio(param.prod.name, '[AWACS] '..callsign, param.prod.freq..' AM')
				end

				local point = trigger.misc.getZone(param.target.name).point
				product.lastMission = { zoneName = param.target.name }
				TaskExtensions.executeAwacsMission(gr, point, param.prod.altitude, param.prod.freq)
				
			end
		end, {prod=product, target=zn}, timer.getTime()+1)

		env.info("ZoneCommand - "..product.name.." targeting "..zn.name)
	end

	function ZoneCommand:isTankerMissionValid(product, target)
		if target.side ~= product.side then return false end
		if target.name == self.name then return false end
		if not target.distToFront or target.distToFront ~= 4 then return false end
		
		return true
	end

	function ZoneCommand:activateTankerMission(product)

		local tgtzones = {}
		for _,v in pairs(ZoneCommand.getAllZones()) do
			if self:isTankerMissionValid(product, v) then
				table.insert(tgtzones, v)
			end
		end

		local choice1 = math.random(1,#tgtzones)
		local zn = tgtzones[choice1]
		table.remove(tgtzones, choice1)

		local og = Utils.getOriginalGroup(product.name)
		if og then
			teleportToPos(product.name, {x=og.x, z=og.y})
			env.info("ZoneCommand - activateTankerMission teleporting to OG pos")
		else
			mist.respawnGroup(product.name, true)
			env.info("ZoneCommand - activateTankerMission fallback to respawnGroup")
		end

		DependencyManager.get("GroupMonitor"):registerGroup(product, zn, self)

		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.prod.name)
			if gr then
				local un = gr:getUnit(1)
				if un then 
					local callsign = un:getCallsign()
					RadioFrequencyTracker.registerRadio(param.prod.name, '[Tanker('..param.prod.variant..')] '..callsign, param.prod.freq..' AM | TCN '..param.prod.tacan..'X')
				end

				local point = trigger.misc.getZone(param.target.name).point
				product.lastMission = { zoneName = param.target.name }
				TaskExtensions.executeTankerMission(gr, point, param.prod.altitude, param.prod.freq, param.prod.tacan)
			end
		end, {prod=product, target=zn}, timer.getTime()+1)
		
		env.info("ZoneCommand - "..product.name.." targeting "..zn.name)
	end

	function ZoneCommand:isPatrolMissionValid(product, target)
		--if target.side ~= product.side then return false end
		if target.name == self.name then return false end
		if not target.distToFront or target.distToFront > 1 then return false end
		if target.side ~= product.side and target.side ~= 0 then return false end
		local dist = mist.utils.get2DDist(self.zone.point, target.zone.point)
		if dist > 150000 then return false end
		
		return true
	end

	function ZoneCommand:activatePatrolMission(product)
		local tgtzones = {}
		for _,v in pairs(ZoneCommand.getAllZones()) do
			if self:isPatrolMissionValid(product, v) then
				table.insert(tgtzones, v)
			end
		end

		local choice1 = math.random(1,#tgtzones)
		local zn1 = tgtzones[choice1]

		if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then
			local prioZone = BattlefieldManager.priorityZones[self.side]
			if self:isPatrolMissionValid(product, prioZone) then
				zn1 = prioZone
			end
		end

		local og = Utils.getOriginalGroup(product.name)
		if og then
			teleportToPos(product.name, {x=og.x, z=og.y})
			env.info("ZoneCommand - activatePatrolMission teleporting to OG pos")
		else
			mist.respawnGroup(product.name, true)
			env.info("ZoneCommand - activatePatrolMission fallback to respawnGroup")
		end

		DependencyManager.get("GroupMonitor"):registerGroup(product, zn1, self)

		if zn1 then 
			product.lastMission = {zone1name = zn1.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)

				local point = trigger.misc.getZone(param.zone1.name).point

				TaskExtensions.executePatrolMission(gr, point, param.prod.altitude, param.prod.range)
			end, {prod=product, zone1 = zn1}, timer.getTime()+1)

			env.info("ZoneCommand - "..product.name.." targeting "..zn1.name)
		end
	end

	function ZoneCommand:isSeadMissionValid(product, target)
		if target.side == 0 then return false end
		if not target.distToFront or target.distToFront > 1 then return false end
		
		--if MissionTargetRegistry.isZoneTargeted(target.name) then return false end

		return target:hasEnemySAMRadar(product)
	end

	function ZoneCommand:hasEnemySAMRadar(product)
		if product.side == 1 then
			return self:hasSAMRadarOnSide(2)
		elseif product.side == 2 then
			return self:hasSAMRadarOnSide(1)
		end
	end

	function ZoneCommand:hasSAMRadarOnSide(side)
		for i,v in pairs(self.built) do
			if v.type == ZoneCommand.productTypes.defense and v.side == side then 
				local gr = Group.getByName(v.name)
				if gr then
					for _,unit in ipairs(gr:getUnits()) do
						if unit:hasAttribute('SAM SR') or unit:hasAttribute('SAM TR') then
							return true
						end
					end
				end
			end
		end
	end

	function ZoneCommand:hasRunway()
		local zones = self:getRunwayZones()
		return #zones > 0
	end

	function ZoneCommand:getRunwayZones()
		local runways = {}
		for i=1,10,1 do
			local name = self.name..'-runway-'..i
			local zone = trigger.misc.getZone(name)
			if zone then 
				runways[i] = {name = name, zone = zone}
			else
				break
			end
		end

		return runways
	end

	function ZoneCommand:getRandomUnitWithAttributeOnSide(attributes, side)
		local available = {}
		for i,v in pairs(self.built) do
			if v.type == ZoneCommand.productTypes.upgrade and v.side == side then
				local st = StaticObject.getByName(v.name)
				if st then
					for _,a in ipairs(attributes) do
						if a == "Buildings" and ZoneCommand.staticRegistry[v.name] then -- dcs does not consider all statics buildings so we compensate
							table.insert(available, v)
						end
					end
				end
			elseif v.type == ZoneCommand.productTypes.defense and v.side == side then 
				local gr = Group.getByName(v.name)
				if gr then
					for _,unit in ipairs(gr:getUnits()) do
						for _,a in ipairs(attributes) do
							if unit:hasAttribute(a) then
								table.insert(available, v)
							end
						end
					end
				end
			end
		end

		if #available > 0 then
			return available[math.random(1, #available)]
		end
	end

	function ZoneCommand:hasUnitWithAttributeOnSide(attributes, side, amount)
		local count = 0

		for i,v in pairs(self.built) do
			if v.type == ZoneCommand.productTypes.upgrade and v.side == side then
				local st = StaticObject.getByName(v.name)
				if st and st:isExist() then
					for _,a in ipairs(attributes) do
						if a == "Buildings" and ZoneCommand.staticRegistry[v.name] then -- dcs does not consider all statics buildings so we compensate
							if amount==nil then
								return true
							else
								count = count + 1
								if count >= amount then return true end
							end
						end
					end
				end
			elseif v.type == ZoneCommand.productTypes.defense and v.side == side then 
				local gr = Group.getByName(v.name)
				if gr then
					for _,unit in ipairs(gr:getUnits()) do
						if unit:isExist() then
							for _,a in ipairs(attributes) do
								if unit:hasAttribute(a) then
									if amount==nil then
										return true
									else
										count = count + 1
										if count >= amount then return true end
									end
								end
							end
						end
					end
				end
			end
		end
	end

	function ZoneCommand:getUnitCountWithAttributeOnSide(attributes, side)
		local count = 0

		for i,v in pairs(self.built) do
			if v.type == ZoneCommand.productTypes.upgrade and v.side == side then
				local st = StaticObject.getByName(v.name)
				if st then
					for _,a in ipairs(attributes) do
						if a == "Buildings" and ZoneCommand.staticRegistry[v.name] then
							count = count + 1
							break
						end
					end
				end
			elseif v.type == ZoneCommand.productTypes.defense and v.side == side then 
				local gr = Group.getByName(v.name)
				if gr then
					for _,unit in ipairs(gr:getUnits()) do
						for _,a in ipairs(attributes) do
							if unit:hasAttribute(a) then
								count = count + 1
								break
							end
						end
					end
				end
			end
		end

		return count
	end

	function ZoneCommand:activateSeadMission(product)
		local tgtzones = {}
		for _,v in pairs(ZoneCommand.getAllZones()) do
			if self:isSeadMissionValid(product, v) then
				table.insert(tgtzones, v)
			end
		end
		
		local choice = math.random(1,#tgtzones)
		local target = tgtzones[choice]

		if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then
			local prioZone = BattlefieldManager.priorityZones[self.side]
			if self:isSeadMissionValid(product, prioZone) then
				target = prioZone
			end
		end

		local og = Utils.getOriginalGroup(product.name)
		if og then
			teleportToPos(product.name, {x=og.x, z=og.y})
			env.info("ZoneCommand - activateSeadMission teleporting to OG pos")
		else
			mist.respawnGroup(product.name, true)
			env.info("ZoneCommand - activateSeadMission fallback to respawnGroup")
		end

		DependencyManager.get("GroupMonitor"):registerGroup(product, target, self)

		if target then 
			product.lastMission = {zoneName = target.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				TaskExtensions.executeSeadMission(gr, param.targets, param.prod.expend, param.prod.altitude)
			end, {prod=product, targets=target.built}, timer.getTime()+1)
			
			env.info("ZoneCommand - "..product.name.." targeting "..target.name)
		end
	end

	function ZoneCommand:isStrikeMissionValid(product, target)
		if target.side == 0 then return false end
		if target.side == product.side then return false end
		if not target.distToFront or target.distToFront > 0 then return false end

		if target:hasEnemySAMRadar(product) then return false end

		--if MissionTargetRegistry.isZoneTargeted(target.name) then return false end

		for i,v in pairs(target.built) do
			if v.type == ZoneCommand.productTypes.upgrade and v.side ~= product.side then 
				return true
			end
		end
	end

	function ZoneCommand:activateStrikeMission(product)
		local tgtzones = {}
		for _,v in pairs(ZoneCommand.getAllZones()) do
			if self:isStrikeMissionValid(product, v) then
				table.insert(tgtzones, v)
			end
		end
		
		local choice = math.random(1,#tgtzones)
		local target = tgtzones[choice]

		if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then
			local prioZone = BattlefieldManager.priorityZones[self.side]
			if self:isStrikeMissionValid(product, prioZone) then
				target = prioZone
			end
		end

		local og = Utils.getOriginalGroup(product.name)
		if og then
			teleportToPos(product.name, {x=og.x, z=og.y})
			env.info("ZoneCommand - activateStrikeMission teleporting to OG pos")
		else
			mist.respawnGroup(product.name, true)
			env.info("ZoneCommand - activateStrikeMission fallback to respawnGroup")
		end

		DependencyManager.get("GroupMonitor"):registerGroup(product, target, self)

		if target then 
			product.lastMission = {zoneName = target.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				TaskExtensions.executeStrikeMission(gr, param.targets, param.prod.expend, param.prod.altitude)
			end, {prod=product, targets=target.built}, timer.getTime()+1)
			
			env.info("ZoneCommand - "..product.name.." targeting "..target.name)
		end
	end

	function ZoneCommand:isCasMissionValid(product, target)
		if target.side == product.side then return false end
		if not target.distToFront or target.distToFront > 0 then return false end

		if target:hasEnemySAMRadar(product) then return false end

		--if MissionTargetRegistry.isZoneTargeted(target.name) then return false end
		
		for i,v in pairs(target.built) do
			if v.type == ZoneCommand.productTypes.defense and v.side ~= product.side then 
				return true
			end
		end
	end

	function ZoneCommand:activateCasMission(product, ishelo)
		local viablezones = {}
		if ishelo then
			viablezones = self.neighbours
		else
			viablezones = ZoneCommand.getAllZones()
		end

		local tgtzones = {}
		for _,v in pairs(viablezones) do
			if self:isCasMissionValid(product, v) then
				table.insert(tgtzones, v)
			end
		end
		
		local choice = math.random(1,#tgtzones)
		local target = tgtzones[choice]

		if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then
			local prioZone = BattlefieldManager.priorityZones[self.side]
			if viablezones[prioZone.name] and self:isCasMissionValid(product, prioZone) then
				target = prioZone
			end
		end

		local og = Utils.getOriginalGroup(product.name)
		if og then
			teleportToPos(product.name, {x=og.x, z=og.y})
			env.info("ZoneCommand - activateCasMission teleporting to OG pos")
		else
			mist.respawnGroup(product.name, true)
			env.info("ZoneCommand - activateCasMission fallback to respawnGroup")
		end

		DependencyManager.get("GroupMonitor"):registerGroup(product, target, self)

		if target then 
			product.lastMission = {zoneName = target.name}
			timer.scheduleFunction(function(param)
				local gr = Group.getByName(param.prod.name)
				if param.helo then
					TaskExtensions.executeHeloCasMission(gr, param.targets, param.prod.expend, param.prod.altitude)
				else
					TaskExtensions.executeCasMission(gr, param.targets, param.prod.expend, param.prod.altitude)
				end
			end, {prod=product, targets=target.built, helo = ishelo}, timer.getTime()+1)

			env.info("ZoneCommand - "..product.name.." targeting "..target.name)
		end
	end
	
	function ZoneCommand:defineUpgrades(upgrades)
		self.upgrades = upgrades
		
		for side,sd in ipairs(self.upgrades) do
			for _,v in ipairs(sd) do
				v.side = side
				
				local cat = TemplateDB.getData(v.template)
				if cat.dataCategory == TemplateDB.type.static then
					ZoneCommand.staticRegistry[v.name] = true
				end
				
				for _,v2 in ipairs(v.products) do
					v2.side = side

					if v2.type == "mission" then
						local gr = Group.getByName(v2.name)
						
						if not gr then
							if v2.missionType ~= ZoneCommand.missionTypes.supply_transfer then
								env.info("ZoneCommand - ERROR declared group does not exist in mission: ".. v2.name)
							end
						else
							gr:destroy() 
						end
					end
				end
			end
		end
	end
	
	function ZoneCommand:getProductByName(name)
		for i,v in ipairs(self.upgrades) do
			for i2,v2 in ipairs(v) do
				if v2.name == name then
					return v2
				else
					for i3,v3 in ipairs(v2.products) do
						if v3.name == name then 
							return v3
						end
					end
				end
			end
		end

		return nil
	end

	function ZoneCommand:cleanup()
		local zn = trigger.misc.getZone(self.name)
		local pos =  {
			x = zn.point.x, 
			y = land.getHeight({x = zn.point.x, y = zn.point.z}), 
			z= zn.point.z
		}
		local radius = zn.radius*2
		world.removeJunk({id = world.VolumeType.SPHERE,params = {point = pos, radius = radius}})
	end
end




-----------------[[ END OF ZoneCommand.lua ]]-----------------



-----------------[[ BattlefieldManager.lua ]]-----------------

BattlefieldManager = {}
do
	BattlefieldManager.closeOverride = 27780 -- 15nm
	BattlefieldManager.farOverride = Config.maxDistFromFront -- default 100nm
	BattlefieldManager.boostScale = {[0] = 1.0, [1]=1.0, [2]=1.0}
	BattlefieldManager.noRedZones = false
	BattlefieldManager.noBlueZones = false

	BattlefieldManager.priorityZones = {
		[1] = nil,
		[2] = nil
	}

	BattlefieldManager.overridePriorityZones = {
		[1] = nil,
		[2] = nil
	}
	
	function BattlefieldManager:new()
		local obj = {}
		
		setmetatable(obj, self)
		self.__index = self
		obj:start()
		return obj
	end
	
	function BattlefieldManager:start()
		timer.scheduleFunction(function(param, time)
			local self = param.context
			
			local zones = ZoneCommand.getAllZones()
			local torank = {}
			
			--reset ranks and define frontline
			for name,zone in pairs(zones) do
				zone.distToFront = nil
				zone.closestEnemyDist = nil
				
				if zone.neighbours then
					for nName, nZone in pairs(zone.neighbours) do
						if zone.side ~= nZone.side then
							zone.distToFront = 0
						end
					end
				end
				
				--set dist to closest enemy
				for name2,zone2 in pairs(zones) do
					if zone.side ~= zone2.side then
						local dist = mist.utils.get2DDist(zone.zone.point, zone2.zone.point)
						if not zone.closestEnemyDist or dist < zone.closestEnemyDist then
							zone.closestEnemyDist = dist
						end
					end
				end
			end
			
			for name,zone in pairs(zones) do
				if zone.distToFront == 0 then
					for nName, nZone in pairs(zone.neighbours) do
						if nZone.distToFrount == nil then
							table.insert(torank, nZone)
						end
					end
				end
			end

			-- build ranks of every other zone
			while #torank > 0 do
				local nexttorank = {}
				for _,zone in ipairs(torank) do
					if not zone.distToFront then
						local minrank = 999
						for nName,nZone in pairs(zone.neighbours) do
							if nZone.distToFront then
								if nZone.distToFront<minrank then
									minrank = nZone.distToFront
								end
							else
								table.insert(nexttorank, nZone)
							end
						end
						zone.distToFront = minrank + 1
					end
				end
				torank = nexttorank
			end
			
			for name, zone in pairs(zones) do
				if zone.keepActive then
					if zone.closestEnemyDist and zone.closestEnemyDist > BattlefieldManager.farOverride and zone.distToFront > 3 then
						zone.mode = ZoneCommand.modes.export
					else
						if zone.mode ~= ZoneCommand.modes.normal then
							zone:fullBuild(1.0)
						end
						zone.mode = ZoneCommand.modes.normal
					end
				else
					if not zone.distToFront or zone.distToFront == 0 or (zone.closestEnemyDist and zone.closestEnemyDist < BattlefieldManager.closeOverride) then
						if zone.mode ~= ZoneCommand.modes.normal then
							zone:fullBuild(1.0)
						end
						zone.mode = ZoneCommand.modes.normal
					elseif zone.distToFront == 1 then
						zone.mode = ZoneCommand.modes.supply
					elseif zone.distToFront > 1 then
						zone.mode = ZoneCommand.modes.export
					end
				end

				zone.boostScale = self.boostScale[zone.side]
			end
			
			return time+60
		end, {context = self}, timer.getTime()+1)

		timer.scheduleFunction(function(param, time)
			local self = param.context
			
			local zones = ZoneCommand.getAllZones()
			
			local noRed = true
			local noBlue = true
			for name, zone in pairs(zones) do
				if zone.side == 1 then
					noRed = false
				elseif zone.side == 2 then
					noBlue = false
				end

				if not noRed and not noBlue then
					break
				end
			end

			if noRed then
				BattlefieldManager.noRedZones = true
			end

			if noBlue then
				BattlefieldManager.noBlueZones = true
			end
			
			return time+10
		end, {context = self}, timer.getTime()+1)

		timer.scheduleFunction(function(param, time)
			local self = param.context
			
			local zones = ZoneCommand.getAllZones()
			
			local frontLineRed = {}
			local frontLineBlue = {}
			for name, zone in pairs(zones) do
				if zone.distToFront == 0 then
					if zone.side == 1 then
						table.insert(frontLineRed, zone)
					elseif zone.side == 2 then
						table.insert(frontLineBlue, zone)
					else
						table.insert(frontLineRed, zone)
						table.insert(frontLineBlue, zone)
					end
				end
			end

			if BattlefieldManager.overridePriorityZones[1] and BattlefieldManager.overridePriorityZones[1].ticks > 0 then
				BattlefieldManager.priorityZones[1] = BattlefieldManager.overridePriorityZones[1].zone
				BattlefieldManager.overridePriorityZones[1].ticks = BattlefieldManager.overridePriorityZones[1].ticks - 1
			else
				local redChangeChance = 1
				if BattlefieldManager.priorityZones[1] and BattlefieldManager.priorityZones[1].side ~= 1 then
					redChangeChance = 0.1
				end

				if #frontLineBlue > 0 then
					if math.random() <= redChangeChance then
						BattlefieldManager.priorityZones[1] = frontLineBlue[math.random(1,#frontLineBlue)]
					end
				else
					BattlefieldManager.priorityZones[1] = nil
				end
			end
			
			if BattlefieldManager.overridePriorityZones[2] and BattlefieldManager.overridePriorityZones[2].ticks > 0 then
				BattlefieldManager.priorityZones[2] = BattlefieldManager.overridePriorityZones[2].zone
				BattlefieldManager.overridePriorityZones[2].ticks = BattlefieldManager.overridePriorityZones[2].ticks - 1
			else
				local blueChangeChance = 1
				if BattlefieldManager.priorityZones[2] and BattlefieldManager.priorityZones[2].side ~= 2 then
					blueChangeChance = 0.1
				end
				
				if #frontLineRed > 0 then
					if math.random() <= blueChangeChance then
						BattlefieldManager.priorityZones[2] = frontLineRed[math.random(1,#frontLineRed)]
					end
				else
					BattlefieldManager.priorityZones[2] = nil
				end
			end

			if BattlefieldManager.priorityZones[1] then
				env.info('BattlefieldManager - red priority: '..BattlefieldManager.priorityZones[1].name)
			else
				env.info('BattlefieldManager - red no priority')
			end

			if BattlefieldManager.priorityZones[2] then
				env.info('BattlefieldManager - blue priority: '..BattlefieldManager.priorityZones[2].name)
			else
				env.info('BattlefieldManager - blue no priority')
			end

			if BattlefieldManager.overridePriorityZones[1] and BattlefieldManager.overridePriorityZones[1].ticks == 0 then
				BattlefieldManager.overridePriorityZones[1] = nil
			end

			if BattlefieldManager.overridePriorityZones[2] and BattlefieldManager.overridePriorityZones[2].ticks == 0 then
				BattlefieldManager.overridePriorityZones[2] = nil
			end
			
			return time+(60*30)
		end, {context = self}, timer.getTime()+10)

		timer.scheduleFunction(function(param, time)
			local x = math.random(-50,50) -- the lower limit benefits blue, higher limit benefits red, adjust to increase limit of random boost variance, default (-50,50)
			local boostIntensity = Config.randomBoost -- adjusts the intensity of the random boost variance, default value = 0.0004
			local factor = (x*x*x*boostIntensity)/100  -- the farther x is the higher the factor, negative beneifts blue, pozitive benefits red
			param.context.boostScale[1] = 1.0+factor
			param.context.boostScale[2] = 1.0-factor

			local red = 0
			local blue = 0
			for i,v in pairs(ZoneCommand.getAllZones()) do
				if v.side == 1 then
					red = red + 1
				elseif v.side == 2 then
					blue = blue + 1
				end

				--v:cleanup()
			end

			-- push factor towards coalition with less zones (up to 0.5)
			local multiplier = Config.lossCompensation -- adjust this to boost losing side production(higher means losing side gains more advantage) (default 1.25)
			local total = red + blue
			local redp = (0.5-(red/total))*multiplier
			local bluep = (0.5-(blue/total))*multiplier

			-- cap factor to avoid increasing difficulty until the end
			redp = math.min(redp, 0.15)
			bluep = math.max(bluep, -0.15)

			param.context.boostScale[1] = param.context.boostScale[1] + redp
			param.context.boostScale[2] = param.context.boostScale[2] + bluep

			--limit to numbers above 0
			param.context.boostScale[1] = math.max(0.01,param.context.boostScale[1])
			param.context.boostScale[2] = math.max(0.01,param.context.boostScale[2])

			env.info('BattlefieldManager - power red = '..param.context.boostScale[1])
			env.info('BattlefieldManager - power blue = '..param.context.boostScale[2])

			return time+(60*30)
		end, {context = self}, timer.getTime()+1)
	end
	
	function BattlefieldManager.overridePriority(side, zone, ticks)
		BattlefieldManager.overridePriorityZones[side] = { zone = zone, ticks = ticks }
		BattlefieldManager.priorityZones[side] = zone
		
		env.info('BattlefieldManager.overridePriority - '..side..' focusing on '..zone.name)
	end
end

-----------------[[ END OF BattlefieldManager.lua ]]-----------------



-----------------[[ Preset.lua ]]-----------------

Preset = {}
do
    function Preset:new(obj)
		setmetatable(obj, self)
		self.__index = self
		return obj
    end

	function Preset:extend(new)
		return Preset:new(Utils.merge(self, new))
	end
end

-----------------[[ END OF Preset.lua ]]-----------------



-----------------[[ PlayerTracker.lua ]]-----------------

PlayerTracker = {}
do
    PlayerTracker.savefile = 'player_stats.json'
    PlayerTracker.statTypes = {
        xp = 'XP',
        cmd = "CMD",
        survivalBonus = "SB"
    }

    PlayerTracker.cmdShopTypes = {
        smoke = 'smoke',
        prio = 'prio',
        jtac = 'jtac',
        bribe1 = 'bribe1',
        bribe2 = 'bribe2',
        artillery = 'artillery',
        sabotage1 = 'sabotage1',
    }

    PlayerTracker.cmdShopPrices = {
        [PlayerTracker.cmdShopTypes.smoke] = 1,
        [PlayerTracker.cmdShopTypes.prio] = 10,
        [PlayerTracker.cmdShopTypes.jtac] = 20,
        [PlayerTracker.cmdShopTypes.bribe1] = 5,
        [PlayerTracker.cmdShopTypes.bribe2] = 10,
        [PlayerTracker.cmdShopTypes.artillery] = 15,
        [PlayerTracker.cmdShopTypes.sabotage1] = 20,
    }

	function PlayerTracker:new()
		local obj = {}
        obj.stats = {}
        obj.config = {}
        obj.tempStats = {}
        obj.groupMenus = {}
        obj.groupShopMenus = {}
        obj.groupConfigMenus = {}
        obj.groupTgtMenus = {}
        obj.playerEarningMultiplier = {}

        if lfs then 
            local dir = lfs.writedir()..'Missions/Saves/'
            lfs.mkdir(dir)
            PlayerTracker.savefile = dir..PlayerTracker.savefile
            env.info('Pretense - Player stats file path: '..PlayerTracker.savefile)
        end

        local save = Utils.loadTable(PlayerTracker.savefile)
        if save then 
            obj.stats = save.stats or {}
            obj.config = save.config or {}
        end

		setmetatable(obj, self)
		self.__index = self
		
        obj:init()

		DependencyManager.register("PlayerTracker", obj)
		return obj
	end

    function PlayerTracker:init()
        local ev = {}
        ev.context = self
        function ev:onEvent(event)
            if not event.initiator then return end
            if not event.initiator.getPlayerName then return end
            if not event.initiator.getCoalition then return end

            local player = event.initiator:getPlayerName()
            if not player then return end
            
            local blocked = false
            if event.id==world.event.S_EVENT_BIRTH then
                if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT and 
                    (Unit.getCategoryEx(event.initiator) == Unit.Category.AIRPLANE or Unit.getCategoryEx(event.initiator) == Unit.Category.HELICOPTER)  then
                    
                        local pname = event.initiator:getPlayerName()
                        if pname then
                            local gr = event.initiator:getGroup()
                            if trigger.misc.getUserFlag(gr:getName())==1 then
                                blocked = true
                                trigger.action.outTextForGroup(gr:getID(), 'Can not spawn as '..gr:getName()..' in enemy/neutral zone or zone without enough resources',5)
                                event.initiator:destroy()

                                for i,v in pairs(net.get_player_list()) do
                                    if net.get_name(v) == pname then
                                        net.send_chat_to('Can not spawn as '..gr:getName()..' in enemy/neutral zone or zone without enough resources' , v)
                                        net.force_player_slot(v, 0, '')
                                        break
                                    end
                                end
                            end
                        end
                end
            end

            if event.id == world.event.S_EVENT_BIRTH and not blocked then
                -- init stats for player if not exist
                if not self.context.stats[player] then
                    self.context.stats[player] = {}
                end

                -- reset temp track for player
                self.context.tempStats[player] = nil

                local minutes = 0
                local multiplier = 1.0
                if self.context.stats[player][PlayerTracker.statTypes.survivalBonus] ~= nil then
                    minutes = self.context.stats[player][PlayerTracker.statTypes.survivalBonus]
                    multiplier = PlayerTracker.minutesToMultiplier(minutes)
                end

                self.context.playerEarningMultiplier[player] = { spawnTime = timer.getAbsTime(), unit = event.initiator, multiplier = multiplier, minutes = minutes }

                local config = self.context:getPlayerConfig(player)
                if config.gci_warning_radius then
                    local gci = DependencyManager.get("GCI")
                    gci:registerPlayer(player, event.initiator, config.gci_warning_radius, config.gci_metric)
                end
            end

            if event.id == world.event.S_EVENT_KILL then
                local target = event.target
                
                if not target then return end
                if not target.getCoalition then return end
                
                if target:getCoalition() == event.initiator:getCoalition() then return end
                
                local xpkey = PlayerTracker.statTypes.xp
                local award = PlayerTracker.getXP(target)

                award = math.floor(award * self.context:getPlayerMultiplier(player))

                local instantxp = math.floor(award*0.25)
                local tempxp = award - instantxp

                self.context:addStat(player, instantxp, PlayerTracker.statTypes.xp)
                local msg = '[XP] '..self.context.stats[player][xpkey]..' (+'..instantxp..')'
                env.info("PlayerTracker.kill - "..player..' awarded '..tostring(instantxp)..' xp')
                
                self.context:addTempStat(player, tempxp, PlayerTracker.statTypes.xp)
                msg = msg..'\n+'..tempxp..' XP (unclaimed)'
                env.info("PlayerTracker.kill - "..player..' awarded '..tostring(tempxp)..' xp (unclaimed)')

                trigger.action.outTextForUnit(event.initiator:getID(), msg, 5)
            end

            if event.id==world.event.S_EVENT_EJECTION then
				self.context.stats[player] = self.context.stats[player] or {}
                local ts = self.context.tempStats[player]
                if ts then
                    local un = event.initiator
                    local key = PlayerTracker.statTypes.xp
                    local xp = self.context.tempStats[player][key]
                    if xp then
                        xp = xp * self.context:getPlayerMultiplier(player)
                        trigger.action.outTextForUnit(un:getID(), 'Ejection. 30\% XP claimed', 5)
                        self.context:addStat(player, math.floor(xp*0.3), PlayerTracker.statTypes.xp)
                        trigger.action.outTextForUnit(un:getID(), '[XP] '..self.context.stats[player][key]..' (+'..math.floor(xp*0.3)..')', 5)
                    end
                    
                    self.context.tempStats[player] = nil
                end
			end

            if event.id==world.event.S_EVENT_TAKEOFF then
                local un = event.initiator
                env.info('PlayerTracker - '..player..' took off in '..tostring(un:getID())..' '..un:getName())
                if self.context.stats[player][PlayerTracker.statTypes.survivalBonus] ~= nil then
                    self.context.stats[player][PlayerTracker.statTypes.survivalBonus] = nil
                    trigger.action.outTextForUnit(un:getID(), 'Taken off, survival bonus no longer secure.', 10)
                end

                local zn = CarrierCommand.getCarrierOfUnit(un:getName())
                if zn then
                    zn:removeResource(Config.carrierSpawnCost)
                else
                    zn = ZoneCommand.getZoneOfUnit(un:getName())
                    if zn then
                        zn:removeResource(Config.zoneSpawnCost)
                    end
                end
			end

            if event.id==world.event.S_EVENT_ENGINE_SHUTDOWN then
                local un = event.initiator
                local zn = ZoneCommand.getZoneOfUnit(un:getName())
                if not zn then 
                    zn = CarrierCommand.getCarrierOfUnit(un:getName())
                end
                if un and un:isExist() and zn and zn.side == un:getCoalition() then
                    env.info('PlayerTracker - '..player..' has shut down engine of '..tostring(un:getID())..' '..un:getName()..' at '..zn.name)
                    self.context.stats[player][PlayerTracker.statTypes.survivalBonus] = self.context:getPlayerMinutes(player)
                    self.context:save()
                    trigger.action.outTextForUnit(un:getID(), 'Engines shut down. Survival bonus secured.', 10)
                    env.info('PlayerTracker - '..player..' secured survival bonus of '..self.context.stats[player][PlayerTracker.statTypes.survivalBonus]..' minutes')
                end
			end

            if event.id==world.event.S_EVENT_LAND then
                self.context:validateLanding(event.initiator, player)
			end
        end

		world.addEventHandler(ev)
        self:periodicSave()
        self:menuSetup()

        timer.scheduleFunction(function(params, time)
            local players = params.context.playerEarningMultiplier
            for i,v in pairs(players) do
                if v.unit.isExist and v.unit:isExist() then
                    if v.multiplier < 5.0 and v.unit and v.unit:isExist() and Utils.isInAir(v.unit) then
                        v.minutes = v.minutes + 1
                        v.multiplier = PlayerTracker.minutesToMultiplier(v.minutes)
                    end
                end
            end

            return time+60
        end, {context = self}, timer.getTime()+60)
    end

    function PlayerTracker:validateLanding(unit, player)
        local un = unit
        local zn = ZoneCommand.getZoneOfUnit(unit:getName())
        if not zn then 
            zn = CarrierCommand.getCarrierOfUnit(unit:getName())
        end

        env.info('PlayerTracker - '..player..' landed in '..tostring(un:getID())..' '..un:getName())
        if un and zn and zn.side == un:getCoalition() then
            trigger.action.outTextForUnit(unit:getID(), "Wait 10 seconds to validate landing...", 10)
            timer.scheduleFunction(function(param, time)
                local un = param.unit
                if not un or not un:isExist() then return end
                
                local player = param.player
                local isLanded = Utils.isLanded(un, true)
                local zn = ZoneCommand.getZoneOfUnit(un:getName())
                if not zn then 
                    zn = CarrierCommand.getCarrierOfUnit(un:getName())
                end

                env.info('PlayerTracker - '..player..' checking if landed: '..tostring(isLanded))

                if isLanded then
                    if zn.isCarrier then
                        zn:addResource(Config.carrierSpawnCost)
                    else
                        zn:addResource(Config.zoneSpawnCost)
                    end

                    if param.context.tempStats[player] then 
                        if zn and zn.side == un:getCoalition() then
                            param.context.stats[player] = param.context.stats[player] or {}
                            
                            trigger.action.outTextForUnit(un:getID(), 'Rewards claimed', 5)
                            for _,key in pairs(PlayerTracker.statTypes) do
                                local value = param.context.tempStats[player][key]
                                env.info("PlayerTracker.landing - "..player..' redeeming '..tostring(value)..' '..key)
                                if value then 
                                    param.context:commitTempStat(player, key)
                                    trigger.action.outTextForUnit(un:getID(), key..' +'..value..'', 5)
                                end
                            end

                            param.context:save()
                        end
                    end
                end
            end, {player = player, unit = unit, context = self}, timer.getTime()+10)
        end
    end

    function PlayerTracker:addTempStat(player, amount, stattype)
        self.tempStats[player] = self.tempStats[player] or {}
        self.tempStats[player][stattype] = self.tempStats[player][stattype] or 0
        self.tempStats[player][stattype] = self.tempStats[player][stattype] + amount
    end

    function PlayerTracker:addStat(player, amount, stattype)
        self.stats[player] = self.stats[player] or {}
        self.stats[player][stattype] = self.stats[player][stattype] or 0

        if stattype == PlayerTracker.statTypes.xp then
            local cur = self:getRank(self.stats[player][stattype])
            if cur then 
                local nxt = self:getRank(self.stats[player][stattype] + amount)
                if nxt and cur.rank < nxt.rank then
                    trigger.action.outText(player..' has leveled up to rank: '..nxt.name, 10)
                    if nxt.cmdAward and nxt.cmdAward > 0 then
                        self:addStat(player, nxt.cmdAward, PlayerTracker.statTypes.cmd)
                        trigger.action.outText(player.." awarded "..nxt.cmdAward.." CMD tokens", 10)
                        env.info("PlayerTracker.addStat - Awarded "..player.." "..nxt.cmdAward.." CMD tokens for rank up to "..nxt.name)
                    end
                end
            end
        end

        self.stats[player][stattype] = self.stats[player][stattype] + amount
    end

    function PlayerTracker:commitTempStat(player, statkey)
        local value = self.tempStats[player][statkey]
        if value then 
            self:addStat(player, value, statkey)

            self.tempStats[player][statkey] = nil
        end
    end

    function PlayerTracker:addRankRewards(player, unit, isTemp)
        local rank = self:getPlayerRank(player)
        if not rank then return end

        local cmdChance = rank.cmdChance
        if cmdChance > 0 then 

            local tkns = 0
            for i=1,rank.cmdTrys,1 do
                local die = math.random()
                if die <= cmdChance then 
                    tkns = tkns + 1
                end
            end

            if tkns > 0 then
                if isTemp then
                    self:addTempStat(player, tkns, PlayerTracker.statTypes.cmd)
                else
                    self:addStat(player, tkns, PlayerTracker.statTypes.cmd)
                end

                local msg = ""
                if isTemp then
                    msg = '+'..tkns..' CMD (unclaimed)'
                else
                    msg = '[CMD] '..self.stats[player][PlayerTracker.statTypes.cmd]..' (+'..tkns..')'
                end

                trigger.action.outTextForUnit(unit:getID(), msg, 5)
                env.info("PlayerTracker.addRankRewards - Awarded "..player.." "..tkns.." CMD tokens with chance "..cmdChance)
            end
        end
    end

    function PlayerTracker.getXP(unit)
        local xp = 30

        if unit:hasAttribute('Planes') then xp = xp + 20 end
        if unit:hasAttribute('Helicopters') then xp = xp + 20 end
        if unit:hasAttribute('Infantry') then xp = xp + 10 end
        if unit:hasAttribute('SAM SR') then xp = xp + 15 end
        if unit:hasAttribute('SAM TR') then xp = xp + 15 end
        if unit:hasAttribute('IR Guided SAM') then xp = xp + 10 end
        if unit:hasAttribute('Ships') then xp = xp + 20 end
        if unit:hasAttribute('Buildings') then xp = xp + 30 end
        if unit:hasAttribute('Tanks') then xp = xp + 10 end

        return xp
    end

    function PlayerTracker:menuSetup()
        
        MenuRegistry:register(1, function(event, context)
			if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
				local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
                    local groupname = event.initiator:getGroup():getName()
					
                    if context.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
                        context.groupMenus[groupid] = nil
                    end

                    if not context.groupMenus[groupid] then
                        
                        local menu = missionCommands.addSubMenuForGroup(groupid, 'Information')
                        missionCommands.addCommandForGroup(groupid, 'Player', menu, Utils.log(context.showGroupStats), context, groupname)
                        missionCommands.addCommandForGroup(groupid, 'Frequencies', menu, Utils.log(context.showFrequencies), context, groupname)
                        missionCommands.addCommandForGroup(groupid, 'Validate Landing', menu, Utils.log(context.validateLandingMenu), context, groupname)
                        
                        context.groupMenus[groupid] = menu
                    end
				end
			elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
					
                    if context.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
                        context.groupMenus[groupid] = nil
                    end
				end
            end
		end, self)

        MenuRegistry:register(5, function(event, context)
			if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
				local player = event.initiator:getPlayerName()
				if player then
                    local rank = context:getPlayerRank(player)
                    if not rank then return end

                    local groupid = event.initiator:getGroup():getID()
                    local groupname = event.initiator:getGroup():getName()
                    
                    if context.groupConfigMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupConfigMenus[groupid])
                        context.groupConfigMenus[groupid] = nil
                    end

                    if not context.groupConfigMenus[groupid] then
                        
                        local menu = missionCommands.addSubMenuForGroup(groupid, 'Config')
                        local missionWarningMenu = missionCommands.addSubMenuForGroup(groupid, 'No mission warning', menu)
                        missionCommands.addCommandForGroup(groupid, 'Activate', missionWarningMenu, Utils.log(context.setNoMissionWarning), context, groupname, true)
                        missionCommands.addCommandForGroup(groupid, 'Deactivate', missionWarningMenu, Utils.log(context.setNoMissionWarning), context, groupname, false)
              
                        context.groupConfigMenus[groupid] = menu
                    end

                    if context.groupShopMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupShopMenus[groupid])
                        context.groupShopMenus[groupid] = nil
                    end
                    
                    if context.groupTgtMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupTgtMenus[groupid])
                        context.groupTgtMenus[groupid] = nil
                    end

                    if not context.groupShopMenus[groupid] then
                        
                        local menu = missionCommands.addSubMenuForGroup(groupid, 'Command & Control')
                        missionCommands.addCommandForGroup(groupid, 'Deploy Smoke ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.smoke]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.smoke)
                        missionCommands.addCommandForGroup(groupid, 'Hack enemy comms ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.bribe1]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.bribe1)
                        missionCommands.addCommandForGroup(groupid, 'Prioritize zone ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.prio]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.prio)
                        missionCommands.addCommandForGroup(groupid, 'Bribe enemy officer ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.bribe2]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.bribe2)
                        missionCommands.addCommandForGroup(groupid, 'Shell zone with artillery ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.artillery]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.artillery)
                        missionCommands.addCommandForGroup(groupid, 'Sabotage enemy zone ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.sabotage1]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.sabotage1)
                        
                        if CommandFunctions.jtac then
                            missionCommands.addCommandForGroup(groupid, 'Deploy JTAC ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.jtac]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.jtac)
                        end

                        context.groupShopMenus[groupid] = menu
                    end
				end
			elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
					
                    if context.groupShopMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupShopMenus[groupid])
                        context.groupShopMenus[groupid] = nil
                    end

                    if context.groupTgtMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupTgtMenus[groupid])
                        context.groupTgtMenus[groupid] = nil
                    end
				end
            end
		end, self)
		
        DependencyManager.get("MarkerCommands"):addCommand('stats',function(event, _, state) 
            local unit = nil
            if event.initiator then 
                unit = event.initiator
            elseif world.getPlayer() then
                unit = world.getPlayer()
            end

            if not unit then return false end

            state:showGroupStats(unit:getGroup():getName())
            return true
        end, false, self)

        DependencyManager.get("MarkerCommands"):addCommand('freqs',function(event, _, state) 
            local unit = nil
            if event.initiator then 
                unit = event.initiator
            elseif world.getPlayer() then
                unit = world.getPlayer()
            end

            if not unit then return false end

            state:showFrequencies(unit:getGroup():getName())
            return true
        end, false, self)
    end

    function PlayerTracker:setNoMissionWarning(groupname, active)
        local gr = Group.getByName(groupname)
        if gr and gr:getSize()>0 then 
            local un = gr:getUnit(1)
            if un then
                local player = un:getPlayerName()
                if player then
                    self:setPlayerConfig(player, "noMissionWarning", active)
                end
            end
        end
    end

    function PlayerTracker:buyCommand(groupname, itemType)
        local gr = Group.getByName(groupname)
        if gr and gr:getSize()>0 then 
            local un = gr:getUnit(1)
            if un then
                local player = un:getPlayerName()
                local cost = PlayerTracker.cmdShopPrices[itemType]
                local cmdTokens = self.stats[player][PlayerTracker.statTypes.cmd]

                if cmdTokens and cost <= cmdTokens then
                    local canPurchase = true

                    if self.groupTgtMenus[gr:getID()] then
                        missionCommands.removeItemForGroup(gr:getID(), self.groupTgtMenus[gr:getID()])
                        self.groupTgtMenus[gr:getID()] = nil
                    end

                    if itemType == PlayerTracker.cmdShopTypes.smoke then

                        self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Smoke Marker target", function(params) 
                            CommandFunctions.smokeTargets(params.zone, 5)
                            trigger.action.outTextForGroup(params.groupid, "Targets marked at "..params.zone.name.." with red smoke", 5)
                        end, 1, 1, nil, nil, true)

                        if self.groupTgtMenus[gr:getID()] then
                            trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10)
                        else
                            trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10)
                            canPurchase = false
                        end

                    elseif itemType == PlayerTracker.cmdShopTypes.jtac then

                        self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "JTAC target", function(params) 

                            CommandFunctions.spawnJtac(params.zone)
                            trigger.action.outTextForGroup(params.groupid, "Reaper orbiting "..params.zone.name,5)

                        end, 1, 1)

                        if self.groupTgtMenus[gr:getID()] then
                            trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10)
                        else
                            trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10)
                            canPurchase = false
                        end

                    elseif itemType== PlayerTracker.cmdShopTypes.prio then

                        self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Priority zone", function(params) 
                            BattlefieldManager.overridePriority(2, params.zone, 4)
                            trigger.action.outTextForGroup(params.groupid, "Blue is concentrating efforts on "..params.zone.name.." for the next two hours", 5)
                        end, nil, 1)

                        if self.groupTgtMenus[gr:getID()] then
                            trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10)
                        else
                            trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10)
                            canPurchase = false
                        end

                    elseif itemType== PlayerTracker.cmdShopTypes.bribe1 then

                        timer.scheduleFunction(function(params, time)
                            local count = 0
                            for i,v in pairs(ZoneCommand.getAllZones()) do
                                if v.side == 1 and v.distToFront <= 1 then
                                    if math.random()<0.5 then
                                        v:reveal()
                                        count = count + 1
                                    end
                                end
                            end
                            if count > 0 then
                                trigger.action.outTextForGroup(params.groupid, "Intercepted enemy communications have revealed information on "..count.." enemy zones",20)
                            else
                                trigger.action.outTextForGroup(params.groupid, "No useful information has been intercepted",20)
                            end
                        end, {groupid=gr:getID()}, timer.getTime()+60)

                        trigger.action.outTextForGroup(gr:getID(), "Attempting to intercept enemy comms...",60)

                    elseif itemType == PlayerTracker.cmdShopTypes.bribe2 then
                        timer.scheduleFunction(function(params, time)
                            local count = 0
                            for i,v in pairs(ZoneCommand.getAllZones()) do
                                if v.side == 1 then
                                    if math.random()<0.5 then
                                        v:reveal()
                                        count = count + 1
                                    end
                                end
                            end

                            if count > 0 then
                                trigger.action.outTextForGroup(params.groupid, "Bribed officer has shared intel on "..count.." enemy zones",20)
                            else
                                trigger.action.outTextForGroup(params.groupid, "Bribed officer has stopped responding to attempted communications.",20)
                            end
                        end, {groupid=gr:getID()}, timer.getTime()+(60*5))
                        
                        trigger.action.outTextForGroup(gr:getID(), "Bribe has been transfered to enemy officer. Waiting for contact...",20)
                    elseif itemType == PlayerTracker.cmdShopTypes.artillery then
                        self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Artillery target", function(params)
                            CommandFunctions.shellZone(params.zone, 50)
                        end, 1, 1)

                        if self.groupTgtMenus[gr:getID()] then
                            trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10)
                        else
                            trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10)
                            canPurchase = false
                        end

                    elseif itemType == PlayerTracker.cmdShopTypes.sabotage1 then
                        self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Sabotage target", function(params)
                            CommandFunctions.sabotageZone(params.zone)
                        end, 1, 1)

                        if self.groupTgtMenus[gr:getID()] then
                            trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10)
                        else
                            trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10)
                            canPurchase = false
                        end
                    end
                    
                    if canPurchase then
                        self.stats[player][PlayerTracker.statTypes.cmd] = self.stats[player][PlayerTracker.statTypes.cmd] - cost
                    end
                else
                    trigger.action.outTextForUnit(un:getID(), "Insufficient CMD to buy selected item", 5)
                end
            end
        end
    end

    function PlayerTracker:showFrequencies(groupname)
        local gr = Group.getByName(groupname)
        if gr then 
            for i,v in pairs(gr:getUnits()) do
                if v.getPlayerName and v:getPlayerName() then
                    local message = RadioFrequencyTracker.getRadioFrequencyMessage(gr:getCoalition())
                    trigger.action.outTextForUnit(v:getID(), message, 20)
                end
            end
        end
    end

    function PlayerTracker:validateLandingMenu(groupname)
        local gr = Group.getByName(groupname)
        if gr then 
            for i,v in pairs(gr:getUnits()) do
                if v.getPlayerName and v:getPlayerName() then
                    self:validateLanding(v, v:getPlayerName())
                end
            end
        end
    end

    function PlayerTracker:showGroupStats(groupname)
        local gr = Group.getByName(groupname)
        if gr then 
            for i,v in pairs(gr:getUnits()) do
                if v.getPlayerName and v:getPlayerName() then
                    local player = v:getPlayerName()
                    local message = '['..player..']\n'
                    
                    local stats = self.stats[player]
                    if stats then
                        local xp = stats[PlayerTracker.statTypes.xp]
                        if xp then
                            local rank, nextRank = self:getRank(xp)
                            
                            message = message ..'\nXP: '..xp

                            if rank then
                                message = message..'\nRank: '..rank.name
                            end

                            if nextRank then
                                message = message..'\nXP needed for promotion: '..(nextRank.requiredXP-xp)
                            end
                        end

                        local multiplier = self:getPlayerMultiplier(player)
                        if multiplier then
                            message = message..'\nSurvival XP multiplier: '..string.format("%.2f", multiplier)..'x'
                            
                            if stats[PlayerTracker.statTypes.survivalBonus] ~= nil then
                                message = message..' [SECURED]'
                            end
                        end

                        local cmd = stats[PlayerTracker.statTypes.cmd]
                        if cmd then
                            message = message ..'\n\nCMD: '..cmd
                        end
                    end

                    local tstats = self.tempStats[player]
                    if tstats then
                        message = message..'\n'
                        local tempxp =  tstats[PlayerTracker.statTypes.xp]
                        if tempxp and tempxp > 0 then
                            message = message .. '\nUnclaimed XP: '..tempxp
                        end

                        local tempcmd =  tstats[PlayerTracker.statTypes.cmd]
                        if tempcmd and tempcmd > 0 then
                            message = message .. '\nUnclaimed CMD: '..tempcmd
                        end
                    end

                    trigger.action.outTextForUnit(v:getID(), message, 10)
                end
            end
        end
    end

    function PlayerTracker:setPlayerConfig(player, setting, value)
        local cfg = self:getPlayerConfig(player)
        cfg[setting] = value
    end

    function PlayerTracker:getPlayerConfig(player)
        if not self.config[player] then
            self.config[player] = {
                noMissionWarning = false,
                gci_warning_radius = nil,
                gci_metric = nil
            }
        end

        return self.config[player]
    end

    function PlayerTracker:periodicSave()
        timer.scheduleFunction(function(param, time)
            param:save()
            return time+60
        end, self, timer.getTime()+60)
    end

    function PlayerTracker:save()
        local tosave = {}
        tosave.stats = self.stats
        tosave.config = self.config
        
        tosave.zones = {}
        tosave.zones.red = {}
        tosave.zones.blue = {}
        tosave.zones.neutral = {}
        for i,v in pairs(ZoneCommand.getAllZones()) do
            if v.side == 1 then
                table.insert(tosave.zones.red,v.name)
            elseif v.side == 2 then
                table.insert(tosave.zones.blue,v.name)
            elseif v.side == 0 then
                table.insert(tosave.zones.neutral,v.name)
            end
        end

        tosave.players = {}
        for i,v in ipairs(coalition.getPlayers(2)) do
            if v and v:isExist() and v.getPlayerName then
                table.insert(tosave.players, {name=v:getPlayerName(), unit=v:getDesc().typeName})
            end
        end

        Utils.saveTable(PlayerTracker.savefile, tosave)
        env.info("PlayerTracker - state saved")
    end

    PlayerTracker.ranks = {}
    PlayerTracker.ranks[1] =  { rank=1,  name='E-1 Airman basic',           requiredXP = 0,        cmdChance = 0,       cmdAward=0,     cmdTrys=0}
    PlayerTracker.ranks[2] =  { rank=2,  name='E-2 Airman',                 requiredXP = 2000,     cmdChance = 0,       cmdAward=0,     cmdTrys=0}
    PlayerTracker.ranks[3] =  { rank=3,  name='E-3 Airman first class',     requiredXP = 4500,     cmdChance = 0,       cmdAward=0,     cmdTrys=0}
    PlayerTracker.ranks[4] =  { rank=4,  name='E-4 Senior airman',          requiredXP = 7700,     cmdChance = 0,       cmdAward=0,     cmdTrys=0}
    PlayerTracker.ranks[5] =  { rank=5,  name='E-5 Staff sergeant',         requiredXP = 11800,    cmdChance = 0.01,    cmdAward=1,     cmdTrys=1}
    PlayerTracker.ranks[6] =  { rank=6,  name='E-6 Technical sergeant',     requiredXP = 17000,    cmdChance = 0.01,    cmdAward=5,     cmdTrys=10}
    PlayerTracker.ranks[7] =  { rank=7,  name='E-7 Master sergeant',        requiredXP = 23500,    cmdChance = 0.03,    cmdAward=5,     cmdTrys=10}
    PlayerTracker.ranks[8] =  { rank=8,  name='E-8 Senior master sergeant', requiredXP = 31500,    cmdChance = 0.06,    cmdAward=10,    cmdTrys=10}
    PlayerTracker.ranks[9] =  { rank=9,  name='E-9 Chief master sergeant',  requiredXP = 42000,    cmdChance = 0.10,    cmdAward=10,    cmdTrys=10}
    PlayerTracker.ranks[10] = { rank=10, name='O-1 Second lieutenant',      requiredXP = 52800,    cmdChance = 0.14,    cmdAward=20,    cmdTrys=15}
    PlayerTracker.ranks[11] = { rank=11, name='O-2 First lieutenant',       requiredXP = 66500,    cmdChance = 0.20,    cmdAward=20,    cmdTrys=15}
    PlayerTracker.ranks[12] = { rank=12, name='O-3 Captain',                requiredXP = 82500,    cmdChance = 0.27,    cmdAward=25,    cmdTrys=15, allowCarrierSupport=true}
    PlayerTracker.ranks[13] = { rank=13, name='O-4 Major',                  requiredXP = 101000,   cmdChance = 0.34,    cmdAward=25,    cmdTrys=20, allowCarrierSupport=true}
    PlayerTracker.ranks[14] = { rank=14, name='O-5 Lieutenant colonel',     requiredXP = 122200,   cmdChance = 0.43,    cmdAward=25,    cmdTrys=20, allowCarrierSupport=true}
    PlayerTracker.ranks[15] = { rank=15, name='O-6 Colonel',                requiredXP = 146300,   cmdChance = 0.52,    cmdAward=30,    cmdTrys=20, allowCarrierSupport=true}
    PlayerTracker.ranks[16] = { rank=16, name='O-7 Brigadier general',      requiredXP = 173500,   cmdChance = 0.63,    cmdAward=35,    cmdTrys=25, allowCarrierSupport=true, allowCarrierCommand=true}
    PlayerTracker.ranks[17] = { rank=17, name='O-8 Major general',          requiredXP = 204000,   cmdChance = 0.74,    cmdAward=40,    cmdTrys=25, allowCarrierSupport=true, allowCarrierCommand=true}
    PlayerTracker.ranks[18] = { rank=18, name='O-9 Lieutenant general',     requiredXP = 238000,   cmdChance = 0.87,    cmdAward=45,    cmdTrys=25, allowCarrierSupport=true, allowCarrierCommand=true}
    PlayerTracker.ranks[19] = { rank=19, name='O-10 General',               requiredXP = 275700,   cmdChance = 0.95,    cmdAward=50,    cmdTrys=30, allowCarrierSupport=true, allowCarrierCommand=true}

    function PlayerTracker:getPlayerRank(playername)
        if self.stats[playername] then
            local xp = self.stats[playername][PlayerTracker.statTypes.xp]
            if xp then
                return self:getRank(xp)
            end
        end
    end

    function PlayerTracker:getPlayerMultiplier(playername)
        if self.playerEarningMultiplier[playername] then
            return self.playerEarningMultiplier[playername].multiplier
        end

        return 1.0
    end

    function PlayerTracker:getPlayerMinutes(playername)
        if self.playerEarningMultiplier[playername] then
            return self.playerEarningMultiplier[playername].minutes
        end

        return 0
    end

    function PlayerTracker.minutesToMultiplier(minutes)
        local multi = 1.0
        if minutes > 10 and minutes <= 60 then
            multi = 1.0 + ((minutes-10)*0.05)
        elseif minutes > 60 then
            multi = 1.0 + (50*0.05) + ((minutes - 60)*0.025)
        end

        return math.min(multi, 5.0)
    end

    function PlayerTracker:getRank(xp)
        local rank = nil
        local nextRank = nil
        for _, rnk in ipairs(PlayerTracker.ranks) do
            if rnk.requiredXP <= xp then
                rank = rnk
            else
                nextRank = rnk
                break
            end
        end

        return rank, nextRank
    end
end

-----------------[[ END OF PlayerTracker.lua ]]-----------------



-----------------[[ ReconManager.lua ]]-----------------

ReconManager = {}
do
    ReconManager.groupMenus = {}
    ReconManager.requiredProgress = 5*60
    ReconManager.updateFrequency = 5

    function ReconManager:new()
        local obj = {}
        obj.recondata = {}
        obj.cancelRequests = {}

        setmetatable(obj, self)
		self.__index = self
		DependencyManager.register("ReconManager", obj)
        obj:init()
        return obj
    end

    function ReconManager:init()
        MenuRegistry:register(7, function(event, context)
            if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
                if player then
                    local groupid = event.initiator:getGroup():getID()
                    local groupname = event.initiator:getGroup():getName()
                    
                    if ReconManager.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, ReconManager.groupMenus[groupid])
                        ReconManager.groupMenus[groupid] = nil
                    end
    
                    if not ReconManager.groupMenus[groupid] then
                        local menu = missionCommands.addSubMenuForGroup(groupid, 'Recon')
                        missionCommands.addCommandForGroup(groupid, 'Start', menu, Utils.log(context.activateRecon), context, groupname)
                        missionCommands.addCommandForGroup(groupid, 'Cancel', menu, Utils.log(context.cancelRecon), context, groupname)
                        missionCommands.addCommandForGroup(groupid, 'Analyze', menu, Utils.log(context.analyzeData), context, groupname)
    
                        ReconManager.groupMenus[groupid] = menu
                    end
                end
            elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
                if player then
                    local groupid = event.initiator:getGroup():getID()
                    
                    if ReconManager.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, ReconManager.groupMenus[groupid])
                        ReconManager.groupMenus[groupid] = nil
                    end
                end
            end
        end, self)
    end

    function ReconManager:activateRecon(groupname)
        local gr = Group.getByName(groupname)
		if gr then
			local un = gr:getUnit(1)
			if un and un:isExist() then
                timer.scheduleFunction(function(param, time)
                    local cancelRequest = param.context.cancelRequests[param.groupname]
                    if cancelRequest and (timer.getAbsTime() - cancelRequest < 5) then
                        param.context.cancelRequests[param.groupname] = nil
                        return
                    end

                    local shouldUpdateMsg = (timer.getAbsTime() - param.lastUpdate) > ReconManager.updateFrequency

                    local withinParameters = false

                    local pgr = Group.getByName(param.groupname)
                    if not pgr then 
                        return 
                    end
                    local pun = pgr:getUnit(1)
                    if not pun or not pun:isExist() then 
                        return
                    end

                    local closestZone = nil
                    if param.lastZone then
                        if param.lastZone.side == 0 or param.lastZone.side == pun:getCoalition() then
                            local msg = param.lastZone.name..' is no longer controlled by the enemy.'
                            msg = msg..'\n Discarding data.'
                            trigger.action.outTextForUnit(pun:getID(), msg, 20)
                            closestZone = ZoneCommand.getClosestZoneToPoint(pun:getPoint(), Utils.getEnemy(pun:getCoalition()))
                        else
                            closestZone = param.lastZone
                        end
                    else
                        closestZone = ZoneCommand.getClosestZoneToPoint(pun:getPoint(), Utils.getEnemy(pun:getCoalition()))
                    end
                    
                    if not closestZone then
                        return
                    end

                    local stats = ReconManager.getAircraftStats(pun:getDesc().typeName)
                    local currentParameters = {
                        distance = 0,
                        deviation = 0,
                        percent_visible = 0
                    }
                    
                    currentParameters.distance = mist.utils.get2DDist(pun:getPoint(), closestZone.zone.point)

                    local unitPos = pun:getPosition()
                    local unitheading = math.deg(math.atan2(unitPos.x.z, unitPos.x.x))
                    local bearing = Utils.getBearing(pun:getPoint(), closestZone.zone.point)
                    
                    currentParameters.deviation = math.abs(Utils.getHeadingDiff(unitheading, bearing))

                    local unitsCount = 0
                    local visibleCount = 0
                    for _,product in pairs(closestZone.built) do
                        if product.side ~= pun:getCoalition() then
                            local gr = Group.getByName(product.name)
                            if gr then
                                for _,enemyUnit in ipairs(gr:getUnits()) do
                                    unitsCount = unitsCount+1
                                    local from = pun:getPoint()
                                    from.y = from.y+1.5
                                    local to = enemyUnit:getPoint()
                                    to.y = to.y+1.5
                                    if land.isVisible(from, to) then
                                        visibleCount = visibleCount+1
                                    end
                                end 
                            else
                                local st = StaticObject.getByName(product.name)
                                if st then
                                    unitsCount = unitsCount+1
                                    local from = pun:getPoint()
                                    from.y = from.y+1.5
                                    local to = st:getPoint()
                                    to.y = to.y+1.5
                                    if land.isVisible(from, to) then
                                        visibleCount = visibleCount+1
                                    end
                                end
                            end
                        end
                    end
                    
                    if unitsCount > 0 and visibleCount > 0 then
                        currentParameters.percent_visible = visibleCount/unitsCount
                    end

                    if currentParameters.distance < (stats.minDist * 1000) and currentParameters.percent_visible >= 0.5 then
                        if stats.maxDeviation then
                            if currentParameters.deviation <= stats.maxDeviation then
                                withinParameters = true
                            end
                        else
                            withinParameters = true
                        end
                    end

                    if withinParameters then
                        if not param.lastZone then
                            param.lastZone = closestZone
                        end

                        param.timeout = 300

                        local speed = stats.recon_speed * currentParameters.percent_visible
                        param.progress = math.min(param.progress + speed, ReconManager.requiredProgress)

                        if shouldUpdateMsg then
                            local msg = "[Recon: "..param.lastZone.name..']'
                            msg = msg.."\nProgress: "..string.format('%.1f', (param.progress/ReconManager.requiredProgress)*100)..'%\n'
                            msg = msg.."\nVisibility: "..string.format('%.1f', currentParameters.percent_visible*100)..'%'
                            trigger.action.outTextForUnit(pun:getID(), msg, ReconManager.updateFrequency)

                            param.lastUpdate = timer.getAbsTime()
                        end
                    else
                        param.timeout = param.timeout - 1
                        if shouldUpdateMsg then

                            local msg = "[Nearest enemy zone: "..closestZone.name..']'
                            
                            if param.lastZone then
                                msg = "[Recon in progress: "..param.lastZone.name..']'
                                msg = msg.."\nProgress: "..string.format('%.1f', (param.progress/ReconManager.requiredProgress)*100)..'%\n'
                            end
                            
                            if stats.maxDeviation then
                                msg = msg.."\nDeviation: "..string.format('%.1f', currentParameters.deviation)..' deg (under '..stats.maxDeviation..' deg)'
                            end
                            
                            msg = msg.."\nDistance: "..string.format('%.2f', currentParameters.distance/1000)..'km (under '..stats.minDist..' km)'
                            msg = msg.."\nVisibility: "..string.format('%.1f', currentParameters.percent_visible*100)..'% (min 50%)'
                            msg = msg.."\n\nTime left: "..param.timeout..' sec'
                            trigger.action.outTextForUnit(pun:getID(), msg, ReconManager.updateFrequency)

                            param.lastUpdate = timer.getAbsTime()
                        end
                    end

                    if param.progress >= ReconManager.requiredProgress then

                        local msg = "Data recorded for "..param.lastZone.name
                        msg = msg.."\nAnalyze data at a friendly zone to recover results"
                        trigger.action.outTextForUnit(pun:getID(), msg, 20)

                        param.context.recondata[param.groupname] = param.lastZone
                        return
                    end
                    
                    if param.timeout > 0 then
                        return time+1
                    end

                    local msg = "Recon cancelled."
                    if param.progress > 0 then
                        msg = msg.." Data lost."
                    end 
                    trigger.action.outTextForUnit(pun:getID(), msg, 20)

                end, {context = self, groupname = groupname, timeout = 300, progress = 0, lastZone = nil, lastUpdate = timer.getAbsTime()-5}, timer.getTime()+1)
            end
        end
    end

    function ReconManager:cancelRecon(groupname)
        self.cancelRequests[groupname] = timer.getAbsTime()
    end

    function ReconManager:analyzeData(groupname)
        local gr = Group.getByName(groupname)
        if not gr then return end
        local un = gr:getUnit(1)
        if not un or not un:isExist() then return end
        local player = un:getPlayerName()
        
        local zn = ZoneCommand.getZoneOfUnit(un:getName())
        if not zn then
            zn = CarrierCommand.getCarrierOfUnit(un:getName())
        end

        if not zn or not Utils.isLanded(un, zn.isCarrier) then 
            trigger.action.outTextForUnit(un:getID(), "Recon data can only be analyzed while landed in a friendly zone.", 5)
            return 
        end

        local data = self.recondata[groupname]
        if data then
            if data.side == 0 or data.side == un:getCoalition() then
                local msg = param.lastZone.name..' is no longer controlled by the enemy.'
                msg = msg..'\n Data discarded.'
                trigger.action.outTextForUnit(un:getID(), msg, 20)
            else
                local wasRevealed = data.revealTime > 60
                data:reveal()

                if data:hasUnitWithAttributeOnSide({'Buildings'}, 1) then
                    local tgt = data:getRandomUnitWithAttributeOnSide({'Buildings'}, 1)
                    if tgt then
                        MissionTargetRegistry.addStrikeTarget(tgt, data)
                        trigger.action.outTextForUnit(un:getID(), tgt.display..' discovered at '..data.name, 20)
                    end
                end

                local xp = RewardDefinitions.actions.recon * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player)
                if wasRevealed then
                    xp = xp/10
                end

                DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp)
                local msg = '+'..math.floor(xp)..' XP'
                trigger.action.outTextForUnit(un:getID(), msg, 10)

                DependencyManager.get("MissionTracker"):tallyRecon(player, data.name, zn.name)
            end

            self.recondata[groupname] = nil
        else
            trigger.action.outTextForUnit(un:getID(), "No data recorded.", 5)
        end
    end

    function ReconManager.getAircraftStats(aircraftType)
        local stats = ReconManager.aircraftStats[aircraftType]
        if not stats then
            stats = { recon_speed = 1, minDist = 5 }
        end

        return stats
    end

    ReconManager.aircraftStats = {
        ['A-10A'] =         { recon_speed = 1,  minDist = 5,  },
        ['A-10C'] =         { recon_speed = 2,  minDist = 20, },
        ['A-10C_2'] =       { recon_speed = 2,  minDist = 20, },
        ['A-4E-C'] =        { recon_speed = 1,  minDist = 5,  },
        ['AJS37'] =         { recon_speed = 10, minDist = 10, },
        ['AV8BNA'] =        { recon_speed = 2,  minDist = 20, },
        ['C-101CC'] =       { recon_speed = 1,  minDist = 5,  },
        ['F-14A-135-GR'] =  { recon_speed = 10, minDist = 5,  },
        ['F-14B'] =         { recon_speed = 10, minDist = 5,  },
        ['F-15C'] =         { recon_speed = 1,  minDist = 5,  },
        ['F-16C_50'] =      { recon_speed = 2,  minDist = 20, },
        ['F-5E-3'] =        { recon_speed = 1,  minDist = 5,  },
        ['F-86F Sabre'] =   { recon_speed = 1,  minDist = 5,  },
        ['FA-18C_hornet'] = { recon_speed = 2,  minDist = 20, },
        ['Hercules'] =      { recon_speed = 1,  minDist = 5,  },
        ['J-11A'] =         { recon_speed = 1,  minDist = 5,  },
        ['JF-17'] =         { recon_speed = 2,  minDist = 20, },
        ['L-39ZA'] =        { recon_speed = 1,  minDist = 5,  },
        ['M-2000C'] =       { recon_speed = 1,  minDist = 5,  },
        ['Mirage-F1BE'] =   { recon_speed = 1,  minDist = 5,  },
        ['Mirage-F1CE'] =   { recon_speed = 1,  minDist = 5,  },
        ['Mirage-F1EE'] =   { recon_speed = 1,  minDist = 5,  },
        ['MiG-15bis'] =     { recon_speed = 1,  minDist = 5,  },
        ['MiG-19P'] =       { recon_speed = 1,  minDist = 5,  },
        ['MiG-21Bis'] =     { recon_speed = 1,  minDist = 5,  },
        ['MiG-29A'] =       { recon_speed = 1,  minDist = 5,  },
        ['MiG-29G'] =       { recon_speed = 1,  minDist = 5,  },
        ['MiG-29S'] =       { recon_speed = 1,  minDist = 5,  },
        ['Su-25'] =         { recon_speed = 1,  minDist = 5,  },
        ['Su-25T'] =        { recon_speed = 2,  minDist = 10, },
        ['Su-27'] =         { recon_speed = 1,  minDist = 5,  },
        ['Su-33'] =         { recon_speed = 1,  minDist = 5,  },
        ['T-45'] =          { recon_speed = 1,  minDist = 5,  },
        ['AH-64D_BLK_II'] = { recon_speed = 5,  minDist = 15, maxDeviation = 120 },
        ['Ka-50'] =         { recon_speed = 5,  minDist = 15, maxDeviation = 35  },
        ['Ka-50_3'] =       { recon_speed = 5,  minDist = 15, maxDeviation = 35  },
        ['Mi-24P'] =        { recon_speed = 5,  minDist = 10, maxDeviation = 60  },
        ['Mi-8MT'] =        { recon_speed = 1,  minDist = 5,  maxDeviation = 30  },
        ['SA342L'] =        { recon_speed = 5,  minDist = 10, maxDeviation = 120 },
        ['SA342M'] =        { recon_speed = 10, minDist = 15, maxDeviation = 120 },
        ['SA342Minigun'] =  { recon_speed = 2,  minDist = 5,  maxDeviation = 45  },
        ['UH-1H'] =         { recon_speed = 1,  minDist = 5,  maxDeviation = 30  },
        ['UH-60L'] =        { recon_speed = 1,  minDist = 5,  maxDeviation = 30  }
    }
end

-----------------[[ END OF ReconManager.lua ]]-----------------



-----------------[[ MissionTargetRegistry.lua ]]-----------------

MissionTargetRegistry = {}
do
    MissionTargetRegistry.playerTargetZones = {}

    function MissionTargetRegistry.addZone(zone)
        MissionTargetRegistry.playerTargetZones[zone] = true
    end

    function MissionTargetRegistry.removeZone(zone)
        MissionTargetRegistry.playerTargetZones[zone] = nil
    end

    function MissionTargetRegistry.isZoneTargeted(zone)
        return MissionTargetRegistry.playerTargetZones[zone] ~= nil
    end

    MissionTargetRegistry.baiTargets = {}

    function MissionTargetRegistry.addBaiTarget(target)
        MissionTargetRegistry.baiTargets[target.name] = target
        env.info('MissionTargetRegistry - bai target added '..target.name)
    end

    function MissionTargetRegistry.baiTargetsAvailable(coalition)
        local targets = {}
        for i,v in pairs(MissionTargetRegistry.baiTargets) do
            if v.product.side == coalition then
                local tgt = Group.getByName(v.name)

                if not tgt or not tgt:isExist() or tgt:getSize()==0 then
                    MissionTargetRegistry.removeBaiTarget(v)
                elseif not v.state or v.state ~= 'enroute' then
                    MissionTargetRegistry.removeBaiTarget(v)
                else
                    table.insert(targets, v)
                end
            end
        end

        return #targets > 0
    end

    function MissionTargetRegistry.getRandomBaiTarget(coalition)
        local targets = {}
        for i,v in pairs(MissionTargetRegistry.baiTargets) do
            if v.product.side == coalition then
                local tgt = Group.getByName(v.name)

                if not tgt or not tgt:isExist() or tgt:getSize()==0 then
                    MissionTargetRegistry.removeBaiTarget(v)
                elseif not v.state or v.state ~= 'enroute' then
                    MissionTargetRegistry.removeBaiTarget(v)
                else
                    table.insert(targets, v)
                end
            end
        end

        if #targets == 0 then return end

        local dice = math.random(1,#targets)
        
        return targets[dice]
    end

    function MissionTargetRegistry.removeBaiTarget(target)
        MissionTargetRegistry.baiTargets[target.name] = nil
        env.info('MissionTargetRegistry - bai target removed '..target.name)
    end

    MissionTargetRegistry.strikeTargetExpireTime = 60*60
    MissionTargetRegistry.strikeTargets = {}

    function MissionTargetRegistry.addStrikeTarget(target, zone)
        MissionTargetRegistry.strikeTargets[target.name] = {data=target, zone=zone, addedTime = timer.getAbsTime()}
        env.info('MissionTargetRegistry - strike target added '..target.name)
    end

    function MissionTargetRegistry.strikeTargetsAvailable(coalition, isDeep)
        for i,v in pairs(MissionTargetRegistry.strikeTargets) do
            if v.data.side == coalition then
                local tgt = StaticObject.getByName(v.data.name)
                if not tgt then tgt = Group.getByName(v.data.name) end

                if not tgt or not tgt:isExist() then
                    MissionTargetRegistry.removeStrikeTarget(v)
                elseif timer.getAbsTime() - v.addedTime > MissionTargetRegistry.strikeTargetExpireTime then
                    MissionTargetRegistry.removeStrikeTarget(v)
                elseif not isDeep or v.zone.distToFront >= 2 then
                    return true
                end
            end
        end

        return false
    end

    function MissionTargetRegistry.getRandomStrikeTarget(coalition, isDeep)
        local targets = {}
        for i,v in pairs(MissionTargetRegistry.strikeTargets) do
            if v.data.side == coalition then
                local tgt = StaticObject.getByName(v.data.name)
                if not tgt then tgt = Group.getByName(v.data.name) end

                if not tgt or not tgt:isExist() then
                    MissionTargetRegistry.removeStrikeTarget(v)
                elseif timer.getAbsTime() - v.addedTime > MissionTargetRegistry.strikeTargetExpireTime then
                    MissionTargetRegistry.removeStrikeTarget(v)
                elseif not isDeep or v.zone.distToFront >= 2 then
                    table.insert(targets, v)
                end
            end
        end

        if #targets == 0 then return end

        local dice = math.random(1,#targets)

        return targets[dice]
    end

    function MissionTargetRegistry.getAllStrikeTargets(coalition)
        local targets = {}
        for i,v in pairs(MissionTargetRegistry.strikeTargets) do
            if v.data.side == coalition then
                local tgt = StaticObject.getByName(v.data.name)
                if not tgt then tgt = Group.getByName(v.data.name) end

                if not tgt or not tgt:isExist() then
                    MissionTargetRegistry.removeStrikeTarget(v)
                elseif timer.getAbsTime() - v.addedTime > MissionTargetRegistry.strikeTargetExpireTime then
                    MissionTargetRegistry.removeStrikeTarget(v)
                else
                    table.insert(targets, v)
                end
            end
        end

        return targets
    end

    function MissionTargetRegistry.removeStrikeTarget(target)
        MissionTargetRegistry.strikeTargets[target.data.name] = nil
        env.info('MissionTargetRegistry - strike target removed '..target.data.name)
    end

    MissionTargetRegistry.extractableSquads = {}

    function MissionTargetRegistry.addSquad(squad)
        MissionTargetRegistry.extractableSquads[squad.name] = squad
        env.info('MissionTargetRegistry - squad added '..squad.name)
    end

    function MissionTargetRegistry.squadsReadyToExtract(onside)
        for i,v in pairs(MissionTargetRegistry.extractableSquads) do
            local gr = Group.getByName(i)
            if gr and gr:isExist() and gr:getSize() > 0 and gr:getCoalition() == onside then
                return true
            end
        end

        return false
    end

    function MissionTargetRegistry.getRandomSquad(onside)
        local targets = {}
        for i,v in pairs(MissionTargetRegistry.extractableSquads) do
            local gr = Group.getByName(i)
            if gr and gr:isExist() and gr:getSize() > 0 and gr:getCoalition() == onside then
                table.insert(targets, v)
            end
        end

        if #targets == 0 then return end

        local dice = math.random(1,#targets)

        return targets[dice]
    end

    function MissionTargetRegistry.removeSquad(squad)
        MissionTargetRegistry.extractableSquads[squad.name] = nil
        env.info('MissionTargetRegistry - squad removed '..squad.name)
    end

    MissionTargetRegistry.extractablePilots = {}

    function MissionTargetRegistry.addPilot(pilot)
        MissionTargetRegistry.extractablePilots[pilot.name] = pilot
        env.info('MissionTargetRegistry - pilot added '..pilot.name)
    end

    function MissionTargetRegistry.pilotsAvailableToExtract()
        for i,v in pairs(MissionTargetRegistry.extractablePilots) do
            if v.pilot:isExist() and v.pilot:getSize() > 0 and v.remainingTime > 30*60 then
                return true
            end
        end

        return false
    end

    function MissionTargetRegistry.getRandomPilot()
        local targets = {}
        for i,v in pairs(MissionTargetRegistry.extractablePilots) do
            if v.pilot:isExist() and v.pilot:getSize() > 0 and v.remainingTime > 30*60 then
                table.insert(targets, v)
            end
        end

        if #targets == 0 then return end

        local dice = math.random(1,#targets)

        return targets[dice]
    end

    function MissionTargetRegistry.removePilot(pilot)
        MissionTargetRegistry.extractablePilots[pilot.name] = nil
        env.info('MissionTargetRegistry - pilot removed '..pilot.name)
    end
end

-----------------[[ END OF MissionTargetRegistry.lua ]]-----------------



-----------------[[ RadioFrequencyTracker.lua ]]-----------------

RadioFrequencyTracker = {}

do
    RadioFrequencyTracker.radios = {}

    function RadioFrequencyTracker.registerRadio(groupname, name, frequency)
        RadioFrequencyTracker.radios[groupname] = {name = name, frequency = frequency}    
    end

    function RadioFrequencyTracker.getRadioFrequencyMessage(side)
        local radios ={}
        for i,v in pairs(RadioFrequencyTracker.radios) do
            local gr = Group.getByName(i) 
            if gr and gr:getCoalition()==side then
                table.insert(radios, v)
            else
                RadioFrequencyTracker.radios[i] = nil
            end
        end

        table.sort(radios, function (a,b) return a.name < b.name end)

        local msg = 'Active frequencies:'
        for i,v in ipairs(radios) do
            msg = msg..'\n  '..v.name..' ['..v.frequency..']'
        end

        return msg
    end
end


-----------------[[ END OF RadioFrequencyTracker.lua ]]-----------------



-----------------[[ PersistenceManager.lua ]]-----------------

PersistenceManager = {}

do

    function PersistenceManager:new(path)
        local obj = {
            path = path,
            data = nil
        }

        setmetatable(obj, self)
		self.__index = self
        return obj
    end

    function PersistenceManager:restore()
        self:restoreZones()
        self:restoreAIMissions()
        self:restoreBattlefield()
        self:restoreCsar()
        self:restoreSquads()
        self:restoreCarriers()

        timer.scheduleFunction(function(param)
            param:restoreStrikeTargets()
        end, self, timer.getTime()+5)
    end

    function PersistenceManager:restoreZones()
        local save = self.data
        for i,v in pairs(save.zones) do
            local z = ZoneCommand.getZoneByName(i)
            if z then
                z:setSide(v.side)
                z.resource = v.resource
                z.revealTime = v.revealTime
                z.extraBuildResources = v.extraBuildResources
                z.mode = v.mode
                z.distToFront = v.distToFront
                z.closestEnemyDist = v.closestEnemyDist
                for name,data in pairs(v.built) do
                    local pr = z:getProductByName(name)
                    z:instantBuild(pr)
    
                    if pr.type == 'defense' and type(data) == "table" then
                        local unitTypes = {}
                        for _,typeName in ipairs(data) do
                            if not unitTypes[typeName] then 
                                unitTypes[typeName] = 0
                            end
                            unitTypes[typeName] = unitTypes[typeName] + 1
                        end
    
                        timer.scheduleFunction(function(param, time)
                            local gr = Group.getByName(param.name)
                            if gr then
                                local types = param.data
                                local toKill = {}
                                for _,un in ipairs(gr:getUnits()) do
                                    local tp = un:getDesc().typeName
                                    if types[tp] and types[tp] > 0 then
                                        types[tp] = types[tp] - 1
                                    else
                                        table.insert(toKill, un)
                                    end
                                end
    
                                for _,un in ipairs(toKill) do
                                    un:destroy()
                                end
                            end
                        end, {data=unitTypes, name=name}, timer.getTime()+2)
                    end
                end
                
                if v.currentBuild then
                    local pr = z:getProductByName(v.currentBuild.name)
                    z:queueBuild(pr, v.currentBuild.side, v.currentBuild.isRepair, v.currentBuild.progress)
                end
    
                if v.currentMissionBuild then
                    local pr = z:getProductByName(v.currentMissionBuild.name)
                    z:queueBuild(pr, v.currentMissionBuild.side, false, v.currentMissionBuild.progress)
                end
                
                z:refreshText()
                z:refreshSpawnBlocking()
            end
        end

    end

    function PersistenceManager:restoreAIMissions()
        local save = self.data
        local instantBuildStates = {
            ['uninitialized'] = true,
            ['takeoff'] = true,
        }
    
        local reActivateStates = {
            ['inair'] = true,
            ['enroute'] = true,
            ['atdestination'] = true,
            ['siege'] = true
        }
    
        for i,v in pairs(save.activeGroups) do
            if v.homeName then
                if instantBuildStates[v.state] then
                    local z = ZoneCommand.getZoneByName(v.homeName)
                    if z then
                        local pr = z:getProductByName(v.productName)
                        if z.side == pr.side then
                            z:instantBuild(pr)
                        end
                    end
                elseif v.lastMission and reActivateStates[v.state] then
                    timer.scheduleFunction(function(param, time)
                        local z = ZoneCommand.getZoneByName(param.homeName)
                        if z then
                            z:reActivateMission(param)
                        end
                    end, v, timer.getTime()+3)
                end
            end
        end
    end

    function PersistenceManager:restoreBattlefield()
        local save = self.data
        if save.battlefieldManager then
            if save.battlefieldManager.priorityZones then
                if save.battlefieldManager.priorityZones['1'] then
                    BattlefieldManager.priorityZones[1] = ZoneCommand.getZoneByName(save.battlefieldManager.priorityZones[1])
                end


                if save.battlefieldManager.priorityZones['2'] then
                    BattlefieldManager.priorityZones[2] = ZoneCommand.getZoneByName(save.battlefieldManager.priorityZones[2])
                end
            end

            if save.battlefieldManager.overridePriorityZones then
                if save.battlefieldManager.overridePriorityZones['1'] then
                    BattlefieldManager.overridePriorityZones[1] = {
                        zone = ZoneCommand.getZoneByName(save.battlefieldManager.overridePriorityZones['1'].zone),
                        ticks = save.battlefieldManager.overridePriorityZones['1'].ticks
                    }
                end

                if save.battlefieldManager.overridePriorityZones['2'] then
                    BattlefieldManager.overridePriorityZones[2] = {
                        zone = ZoneCommand.getZoneByName(save.battlefieldManager.overridePriorityZones['2'].zone),
                        ticks = save.battlefieldManager.overridePriorityZones['2'].ticks
                    }
                end
            end
        end 
    end

    function PersistenceManager:restoreCsar()
        local save = self.data
        if save.csarTracker then
            for i,v in pairs(save.csarTracker) do
                DependencyManager.get("CSARTracker"):restorePilot(v)
            end
        end
    end

    function PersistenceManager:restoreSquads()
        local save = self.data
        if save.squadTracker then
            for i,v in pairs(save.squadTracker) do
                local sdata = nil
                if v.isAISpawned then
                    if v.type == PlayerLogistics.infantryTypes.ambush then
                        sdata = GroupMonitor.aiSquads.ambush[v.side]
                    else 
                        sdata = GroupMonitor.aiSquads.manpads[v.side]
                    end
                else
                    sdata = DependencyManager.get("PlayerLogistics").registeredSquadGroups[v.type]
                end

                if sdata then
                    v.data = sdata
                    DependencyManager.get("SquadTracker"):restoreInfantry(v)
                end
            end
        end
    end

    function PersistenceManager:restoreStrikeTargets()
        local save = self.data
        if save.strikeTargets then
            for i,v in pairs(save.strikeTargets) do
                local zone = ZoneCommand.getZoneByName(v.zname)
                local product = zone:getProductByName(v.pname)

                MissionTargetRegistry.strikeTargets[i] = {
                    data = product,
                    zone = zone,
                    addedTime = timer.getAbsTime() - v.elapsedTime,
                    isDeep = isDeep
                }
            end
        end
    end

    function PersistenceManager:restoreCarriers()
        local save = self.data
        if save.carriers then
            for i,v in pairs(save.carriers) do
                local carrier = CarrierCommand.getCarrierByName(v.name)
                if carrier then 
                    carrier.resource = math.min(v.resource, carrier.maxResource)
                    carrier.weaponStocks = v.weaponStocks or {}
                    carrier:refreshSpawnBlocking()

                    local group = Group.getByName(v.name)
                    if group then
                        local vars = {
                            groupName = group:getName(),
                            point = v.position.p,
                            action = 'teleport',
                            heading = math.atan2(v.position.x.z, v.position.x.x),
                            initTasks = false,
                            route = {}
                        }
                
                        mist.teleportToPoint(vars)

                        timer.scheduleFunction(function(param, time)
                            param:setupRadios()
                        end, carrier, timer.getTime()+3)

                        carrier.navigation.waypoints = v.navigation.waypoints
                        carrier.navigation.currentWaypoint = nil
                        carrier.navigation.nextWaypoint = v.navigation.currentWaypoint
                        carrier.navigation.loop = v.navigation.loop

                        if v.supportFlightStates then
                            for sfsName, sfsData in pairs(v.supportFlightStates) do
                                local sflight = carrier.supportFlights[sfsName]
                                if sflight then
                                    if sfsData.state == CarrierCommand.supportStates.inair and sfsData.targetName and sfsData.position then
                                        local zn = ZoneCommand.getZoneByName(sfsData.targetName)
                                        if not zn then 
                                            zn = CarrierCommand.getCarrierByName(sfsData.targetName)
                                        end

                                        if zn then
                                            CarrierCommand.spawnSupport(sflight, zn, sfsData)
                                        end
                                    elseif sfsData.state == CarrierCommand.supportStates.takeoff and sfsData.targetName then
                                        local zn = ZoneCommand.getZoneByName(sfsData.targetName)
                                        if not zn then 
                                            zn = CarrierCommand.getCarrierByName(sfsData.targetName)
                                        end

                                        if zn then
                                            CarrierCommand.spawnSupport(sflight, zn)
                                        end
                                    end
                                end
                            end
                        end

                        if v.aliveGroupMembers then
                            timer.scheduleFunction(function(param, time)
                                local g = Group.getByName(param.name)
                                if not g then return end
                                local grMembers = g:getUnits()
                                local liveMembers = {}
                                for _, agm in ipairs(param.aliveGroupMembers) do
                                    liveMembers[agm] = true
                                end

                                for _, gm in ipairs(grMembers) do
                                    if not liveMembers[gm:getName()] then
                                        gm:destroy()
                                    end
                                end
                            end, v, timer.getTime()+4)
                        end
                    end
                end
            end
        end
    end

    function PersistenceManager:canRestore()
        if self.data == nil then return false end
        
        local redExist = false
        local blueExist = false
        for _,z in pairs(self.data.zones) do
            if z.side == 1 and not redExist then redExist = true end
            if z.side == 2 and not blueExist then blueExist = true end

            if redExist and blueExist then break end
        end

        if not redExist or not blueExist then return false end

        return true
    end

    function PersistenceManager:load()
        self.data = Utils.loadTable(self.path)
    end

    function PersistenceManager:save()
        local tosave = {}
        
        tosave.zones = {}
        for i,v in pairs(ZoneCommand.getAllZones()) do
        
            tosave.zones[i] = {
                name = v.name,
                side = v.side,
                resource = v.resource,
                mode = v.mode,
                distToFront = v.distToFront,
                closestEnemyDist = v.closestEnemyDist,
                extraBuildResources = v.extraBuildResources,
                revealTime = v.revealTime,
                built = {}
            }
            
            for n,b in pairs(v.built) do
                if b.type == 'defense' then
                    local typeList = {}
                    local gr = Group.getByName(b.name)
                    if gr then
                        for _,unit in ipairs(gr:getUnits()) do
                            table.insert(typeList, unit:getDesc().typeName)
                        end

                        tosave.zones[i].built[n] = typeList
                    end
                else
                    tosave.zones[i].built[n] = true
                end
                
            end
            
            if v.currentBuild then
                tosave.zones[i].currentBuild = {
                    name = v.currentBuild.product.name, 
                    progress = v.currentBuild.progress,
                    side = v.currentBuild.side,
                    isRepair = v.currentBuild.isRepair
                }
            end

            if v.currentMissionBuild then
                tosave.zones[i].currentMissionBuild = {
                    name = v.currentMissionBuild.product.name, 
                    progress = v.currentMissionBuild.progress,
                    side = v.currentMissionBuild.side
                }
            end
        end

        tosave.activeGroups = {}
        for i,v in pairs(DependencyManager.get("GroupMonitor").groups) do
            tosave.activeGroups[i] = {
                productName = v.product.name,
                type = v.product.missionType
            }

            local gr = Group.getByName(v.product.name)
            if gr and gr:getSize()>0 then
                local un = gr:getUnit(1)
                if un then 
                    tosave.activeGroups[i].position = un:getPoint()
                    tosave.activeGroups[i].lastMission = v.product.lastMission
                    tosave.activeGroups[i].heading = math.atan2(un:getPosition().x.z, un:getPosition().x.x)
                end
            end

            if v.spawnedSquad then
                tosave.activeGroups[i].spawnedSquad = true
            end

            if v.target then
                tosave.activeGroups[i].targetName = v.target.name
            end

            if v.home then
                tosave.activeGroups[i].homeName = v.home.name
            end

            if v.state then
                tosave.activeGroups[i].state = v.state
                tosave.activeGroups[i].lastStateDuration = timer.getAbsTime() - v.lastStateTime
            else
                tosave.activeGroups[i].state = 'uninitialized'
                tosave.activeGroups[i].lastStateDuration = 0
            end
        end

        tosave.battlefieldManager = {
            priorityZones = {},
            overridePriorityZones = {}
        }

        if BattlefieldManager.priorityZones[1] then
            tosave.battlefieldManager.priorityZones['1'] = BattlefieldManager.priorityZones[1].name
        end
        
        if BattlefieldManager.priorityZones[2] then
            tosave.battlefieldManager.priorityZones['2'] = BattlefieldManager.priorityZones[2].name
        end
        
        if BattlefieldManager.overridePriorityZones[1] then
            tosave.battlefieldManager.overridePriorityZones['1'] = { 
                zone = BattlefieldManager.overridePriorityZones[1].zone.name, 
                ticks = BattlefieldManager.overridePriorityZones[1].ticks
            }
        end

        if BattlefieldManager.overridePriorityZones[2] then
            tosave.battlefieldManager.overridePriorityZones['2'] = { 
                zone = BattlefieldManager.overridePriorityZones[2].zone.name, 
                ticks = BattlefieldManager.overridePriorityZones[2].ticks
            }
        end


        tosave.csarTracker = {}

        for i,v in pairs(DependencyManager.get("CSARTracker").activePilots) do
            if v.pilot:isExist() and v.pilot:getSize()>0 and v.remainingTime>60 then
                tosave.csarTracker[i] = {
                    name = v.name,
                    remainingTime = v.remainingTime,
                    pos = v.pilot:getUnit(1):getPoint()
                }
            end
        end

        tosave.squadTracker = {}

        for i,v in pairs(DependencyManager.get("SquadTracker").activeInfantrySquads) do
            tosave.squadTracker[i] = {
                state = v.state,
                remainingStateTime = v.remainingStateTime,
                position = v.position,
                name = v.name,
                type = v.data.type,
                side = v.data.side,
				isAISpawned = v.data.isAISpawned,
                discovered = v.discovered
            }
        end

        tosave.carriers = {}
        for cname,cdata in pairs(CarrierCommand.getAllCarriers()) do
            local group = Group.getByName(cdata.name)
            if group and group:isExist() then

                tosave.carriers[cname] = {
                    name = cdata.name,
                    resource = cdata.resource,
                    position = group:getUnit(1):getPosition(),
                    navigation = cdata.navigation,
                    supportFlightStates = {},
                    weaponStocks = cdata.weaponStocks,
                    aliveGroupMembers = {}
                }

                for _, gm in ipairs(group:getUnits()) do
                    table.insert(tosave.carriers[cname].aliveGroupMembers, gm:getName())
                end

                for spname, spdata in pairs(cdata.supportFlights) do
                    tosave.carriers[cname].supportFlightStates[spname] = {
                        name = spdata.name, 
                        state = spdata.state,
                        lastStateDuration = timer.getAbsTime() - spdata.lastStateTime,
                        returning = spdata.returning
                    }

                    if spdata.target then
                        tosave.carriers[cname].supportFlightStates[spname].targetName = spdata.target.name
                    end

                    if spdata.state == CarrierCommand.supportStates.inair then
                        local spgr = Group.getByName(spname)
                        if spgr and spgr:isExist() and spgr:getSize()>0 then
                            local spun = spgr:getUnit(1)
                            if spun and spun:isExist() then
                                tosave.carriers[cname].supportFlightStates[spname].position = spun:getPoint()
                                tosave.carriers[cname].supportFlightStates[spname].heading = math.atan2(spun:getPosition().x.z, spun:getPosition().x.x)
                            end
                        end
                    end
                end
            end
        end

        tosave.strikeTargets = {}
        for i,v in pairs(MissionTargetRegistry.strikeTargets) do
            tosave.strikeTargets[i] = { pname = v.data.name, zname = v.zone.name, elapsedTime = timer.getAbsTime() - v.addedTime, isDeep = v.isDeep }
        end

        Utils.saveTable(self.path, tosave)
    end
end

-----------------[[ END OF PersistenceManager.lua ]]-----------------



-----------------[[ TemplateDB.lua ]]-----------------

TemplateDB = {}

do
    TemplateDB.type = {
        group = 'group',
        static = 'static',
    }

    TemplateDB.templates = {}
    function TemplateDB.getData(objtype)
        return TemplateDB.templates[objtype]
    end

    TemplateDB.templates["pilot-replacement"] = {
        units = { "Soldier M4 GRG" },
        skill = "Good",
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["capture-squad"] = {
        units = {
            "Soldier M4 GRG",
            "Soldier M4 GRG",
            "Soldier M249",
            "Soldier M4 GRG"
        },
        skill = "Good",
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["sabotage-squad"] = {
        units = {
            "Soldier M4 GRG",
            "Soldier M249",
            "Soldier M249",
            "Soldier M4 GRG"
        },
        skill = "Good",
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["ambush-squad"] = {
        units = {
            "Soldier RPG",
            "Soldier RPG",
            "Soldier M249",
            "Soldier M4 GRG",
            "Soldier M4 GRG"
        },
        skill = "Good",
        invisible = true,
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["manpads-squad"] = {
        units = {
            "Soldier M4 GRG",
            "Soldier M249",
            "Soldier stinger",
            "Soldier stinger",
            "Soldier M4 GRG"
        },
        skill = "Good",
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["ambush-squad-red"] = {
        units = {
            "Paratrooper RPG-16",
            "Paratrooper RPG-16",
            "Infantry AK ver2",
            "Infantry AK",
            "Infantry AK ver3"
        },
        skill = "Good",
        invisible = true,
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["manpads-squad-red"] = {
        units = {
            "Infantry AK ver3",
            "Infantry AK ver2",
            "SA-18 Igla manpad",
            "SA-18 Igla manpad",
            "Infantry AK"
        },
        skill = "Good",
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["engineer-squad"] = {
        units = {
            "Soldier M4 GRG",
            "Soldier M4 GRG"
        },
        skill = "Good",
        dataCategory= TemplateDB.type.group
    }
    
    TemplateDB.templates["spy-squad"] = {
        units = {
            "Infantry AK"
        },
        skill = "Good",
        invisible = true,
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["rapier-squad"] = {
        units = {
            "rapier_fsa_blindfire_radar",
            "rapier_fsa_optical_tracker_unit",
            "rapier_fsa_launcher",
            "rapier_fsa_launcher",
            "Soldier M4 GRG",
            "Soldier M4 GRG"
        },
        skill = "Excellent",
        dataCategory= TemplateDB.type.group
    }

    TemplateDB.templates["tent"] = { type="FARP Tent", category="Fortifications", shape="PalatkaB", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["barracks"] = { type="house1arm", category="Fortifications", shape=nil, dataCategory=TemplateDB.type.static }

    TemplateDB.templates["outpost"] = { type="outpost", category="Fortifications", shape=nil, dataCategory=TemplateDB.type.static }

    TemplateDB.templates["ammo-cache"] = { type="FARP Ammo Dump Coating", category="Fortifications", shape="SetkaKP", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["ammo-depot"] = { type=".Ammunition depot", category="Warehouses", shape="SkladC", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["fuel-cache"] = { type="FARP Fuel Depot", category="Fortifications", shape="GSM Rus", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["fuel-tank-small"] = { type="Fuel tank", category="Fortifications", shape="toplivo-bak", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["fuel-tank-big"] = { type="Tank", category="Warehouses", shape="bak", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["chem-tank"] = { type="Chemical tank A", category="Fortifications", shape="him_bak_a", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["factory-1"] = { type="Tech combine", category="Fortifications", shape="kombinat", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["factory-2"] = { type="Workshop A", category="Fortifications", shape="tec_a", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["oil-pump"] = { type="Pump station", category="Fortifications", shape="nasos", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["hangar"] = { type="Hangar A", category="Fortifications", shape="angar_a", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["excavator"] = { type="345 Excavator", category="Fortifications", shape="cat_3451", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["farm-house-1"] = { type="Farm A", category="Fortifications", shape="ferma_a", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["farm-house-2"] = { type="Farm B", category="Fortifications", shape="ferma_b", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["antenna"] = { type="Comms tower M", category="Fortifications", shape="tele_bash_m", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["tv-tower"] = { type="TV tower", category="Fortifications", shape="tele_bash", dataCategory=TemplateDB.type.static }

    TemplateDB.templates["command-center"] = { type=".Command Center", category="Fortifications", shape="ComCenter", dataCategory=TemplateDB.type.static }
    
    TemplateDB.templates["military-staff"] = { type="Military staff", category="Fortifications", shape="aviashtab", dataCategory=TemplateDB.type.static }
end

-----------------[[ END OF TemplateDB.lua ]]-----------------



-----------------[[ Spawner.lua ]]-----------------

Spawner = {}

do
    function Spawner.createPilot(name, pos)
        local groupData = Spawner.getData("pilot-replacement", name, pos, nil, 5, {
            [land.SurfaceType.LAND] = true, 
            [land.SurfaceType.ROAD] = true,
            [land.SurfaceType.RUNWAY] = true,
        })

        return coalition.addGroup(country.id.CJTF_BLUE, Group.Category.GROUND, groupData)
    end

    function Spawner.createObject(name, objType, pos, side, minDist, maxDist, surfaceTypes, zone)
        if zone then
            zone = CustomZone:getByName(zone) -- expand zone name to CustomZone object
        end

        local data = Spawner.getData(objType, name, pos, minDist, maxDist, surfaceTypes, zone)

        if not data then return end

        local cnt = country.id.CJTF_BLUE
        if side == 1 then
            cnt = country.id.CJTF_RED
        end

        if data.dataCategory == TemplateDB.type.static then
            return coalition.addStaticObject(cnt, data)
        elseif data.dataCategory == TemplateDB.type.group then
            return coalition.addGroup(cnt, Group.Category.GROUND, data)
        end
    end

    function Spawner.getUnit(unitType, name, pos, skill, minDist, maxDist, surfaceTypes, zone)
        local nudgedPos = nil
		for i=1,500,1 do
            nudgedPos = mist.getRandPointInCircle(pos, maxDist, minDist)
           
            if zone then
                if zone:isInside(nudgedPos) and surfaceTypes[land.getSurfaceType(nudgedPos)] then
                    break
                end
            else
                if surfaceTypes[land.getSurfaceType(nudgedPos)] then
                    break
                end
            end

            if i==500 then env.info('Spawner - ERROR: failed to find good location') end
		end

        return {
            ["type"] = unitType,
            ["skill"] = skill,
            ["coldAtStart"] = false,
            ["x"] = nudgedPos.x,
            ["y"] = nudgedPos.y,
            ["name"] = name,
            ['heading'] = math.random()*math.pi*2,
            ["playerCanDrive"] = false
        }
    end

    function Spawner.getData(objtype, name, pos, minDist, maxDist, surfaceTypes, zone)
        if not maxDist then maxDist = 150 end
        if not surfaceTypes then surfaceTypes = { [land.SurfaceType.LAND]=true } end

        local data = TemplateDB.getData(objtype)
        if not data then 
            env.info("Spawner - ERROR: cant find group data "..tostring(objtype).." for group name "..name)
            return
        end

        local spawnData = {}

        if data.dataCategory == TemplateDB.type.static then
            if not surfaceTypes[land.getSurfaceType(pos)] then
                for i=1,500,1 do
                    pos = mist.getRandPointInCircle(pos, maxDist)

                    if zone then
                        if zone:isInside(pos) and surfaceTypes[land.getSurfaceType(pos)] then
                            break
                        end
                    else
                        if surfaceTypes[land.getSurfaceType(pos)] then
                            break
                        end
                    end
                    
                    if i==500 then env.info('Spawner - ERROR: failed to find good location') end
                end
            end

            spawnData = {
                ["type"] = data.type,
                ["name"] = name,
                ["shape_name"] = data.shape,
                ["category"] = data.category,
                ["x"] = pos.x,
                ["y"] = pos.y,
                ['heading'] = math.random()*math.pi*2
            }
        elseif data.dataCategory== TemplateDB.type.group then
            spawnData = {
                ["units"] = {},
                ["name"] = name,
                ["task"] = "Ground Nothing",
                ["route"] = {
                    ["points"]={
                        {
                            ["x"] = pos.x,
                            ["y"] = pos.y,
                            ["action"] = "Off Road",
                            ["speed"] = 0,
                            ["type"] = "Turning Point",
                            ["ETA"] = 0,
                            ["formation_template"] = "",
                            ["task"] = Spawner.getDefaultTask(data.invisible)
                        }
                    }
                }
            }

            if data.minDist then
                minDist = data.minDist
            end

            if data.maxDist then
                maxDist = data.maxDist
            end
            
            for i,v in ipairs(data.units) do
                table.insert(spawnData.units, Spawner.getUnit(v, name.."-"..i, pos, data.skill, minDist, maxDist, surfaceTypes, zone))
            end
        end

        spawnData.dataCategory = data.dataCategory
            
        return spawnData
    end

    function Spawner.getDefaultTask(invisible)
        local defTask =  {
            ["id"] = "ComboTask",
            ["params"] = 
            {
                ["tasks"] = 
                {
                    [1] = 
                    {
                        ["enabled"] = true,
                        ["auto"] = false,
                        ["id"] = "WrappedAction",
                        ["number"] = 1,
                        ["params"] = 
                        {
                            ["action"] = 
                            {
                                ["id"] = "Option",
                                ["params"] = 
                                {
                                    ["name"] = 9,
                                    ["value"] = 2,
                                },
                            },
                        },
                    }, 
                    [2] = 
                    {
                        ["enabled"] = true,
                        ["auto"] = false,
                        ["id"] = "WrappedAction",
                        ["number"] = 2,
                        ["params"] = 
                        {
                            ["action"] = 
                            {
                                ["id"] = "Option",
                                ["params"] = 
                                {
                                    ["name"] = 0,
                                    ["value"] = 0,
                                }
                            }
                        }
                    }
                }
            }
        }

        if invisible then 
            table.insert(defTask.params.tasks, {
                ["number"] = 3,
                ["auto"] = false,
                ["id"] = "WrappedAction",
                ["enabled"] = true,
                ["params"] = 
                {
                    ["action"] = 
                    {
                        ["id"] = "SetInvisible",
                        ["params"] = 
                        {
                            ["value"] = true,
                        }
                    }
                }
            })
        end
        
        return defTask
    end
end

-----------------[[ END OF Spawner.lua ]]-----------------



-----------------[[ CommandFunctions.lua ]]-----------------

CommandFunctions = {}

do
    CommandFunctions.jtac = nil

    function CommandFunctions.spawnJtac(zone)
        if CommandFunctions.jtac then
            CommandFunctions.jtac:deployAtZone(zone)
            CommandFunctions.jtac:showMenu()
            CommandFunctions.jtac:setLifeTime(60)
            zone:reveal(60)
        end
    end

    function CommandFunctions.smokeTargets(zone, count)
        local units = {}
        for i,v in pairs(zone.built) do
            local g = Group.getByName(v.name)
            if g and g:isExist() then
                for i2,v2 in ipairs(g:getUnits()) do
                    if v2:isExist() then
                        table.insert(units, v2)
                    end
                end
            else
                local s = StaticObject.getByName(v.name)
                if s and s:isExist() then
                    table.insert(units, s)
                end
            end
        end
        
        local tgts = {}
        for i=1,count,1 do
            if #units > 0 then
                local selected = math.random(1,#units)
                table.insert(tgts, units[selected])
                table.remove(units, selected)
            end
        end
        
        for i,v in ipairs(tgts) do
            local pos = v:getPoint()
            trigger.action.smoke(pos, 1)
        end
    end

    function CommandFunctions.sabotageZone(zone)
        trigger.action.outText("Saboteurs have been dispatched to "..zone.name, 10)
        local delay = math.random(5*60, 7*60)
        local isReveled = zone.revealTime > 0
        timer.scheduleFunction(function(param, time)
            if not param.isRevealed then
                if math.random() < 0.3 then
                    trigger.action.outText("Saboteurs have been caught by the enemy before they could complete their mission", 10)
                    return
                end
            end

            local zone = param.zone
            local units = {}
            for i,v in pairs(zone.built) do
                if v.type == 'upgrade' then
                    local s = StaticObject.getByName(v.name)
                    if s and s:isExist() then
                        table.insert(units, s)
                    end
                end
            end
            
            if #units > 0 then
                local selected = units[math.random(1,#units)]

                timer.scheduleFunction(function(p2, t2)
                    if p2.count > 0 then
                        p2.count = p2.count - 1
                        local offsetPos = {
                            x = p2.pos.x + math.random(-25,25),
                            y = p2.pos.y,
                            z = p2.pos.z + math.random(-25,25)
                        }
            
                        offsetPos.y = land.getHeight({x = offsetPos.x, y = offsetPos.z})
                        trigger.action.explosion(offsetPos, 30)
                        return t2 + 0.05 + (math.random())
                    else
                        trigger.action.explosion(p2.pos, 2000)
                    end
                end, {count = 3, pos = selected:getPoint()}, timer.getTime()+0.5)

                trigger.action.outText("Saboteurs have succesfully triggered explosions at "..zone.name, 10)
            end
        end, { zone = zone , isRevealed = isReveled}, timer.getTime()+delay)
    end

    function CommandFunctions.shellZone(zone, count)
        local minutes = math.random(3,7)
        local seconds = math.random(-30,30)
        local delay = (minutes*60)+seconds

        local isRevealed = zone.revealTime > 0
        trigger.action.outText("Artillery preparing to fire on "..zone.name.." ETA: "..minutes.." minutes", 10)
        
        local positions = {}
        for i,v in pairs(zone.built) do
            local g = Group.getByName(v.name)
            if g and g:isExist() then
                for i2,v2 in ipairs(g:getUnits()) do
                    if v2:isExist() then
                        table.insert(positions, v2:getPoint())
                    end
                end
            else
                local s = StaticObject.getByName(v.name)
                if s and s:isExist() then
                    table.insert(positions, s:getPoint())
                end
            end
        end

        timer.scheduleFunction(function(param, time)
            trigger.action.outText("Artillery firing on "..param.zone.name.." ETA: 30 seconds", 10)
        end, {zone = zone}, timer.getTime()+delay-30)

        timer.scheduleFunction(function(param, time)
            param.count = param.count - 1
            
            local accuracy = 50
            if param.isRevealed then
                accuracy = 10
            end

            local selected = param.positions[math.random(1,#param.positions)]
            local offsetPos = {
                x = selected.x + math.random(-accuracy,accuracy),
                y = selected.y,
                z = selected.z + math.random(-accuracy,accuracy)
            }

            offsetPos.y = land.getHeight({x = offsetPos.x, y = offsetPos.z})

            trigger.action.explosion(offsetPos, 20)

            if param.count > 0 then
                return time+0.05+(math.random()*2)
            else
                trigger.action.outText("Artillery finished firing on "..param.zone.name, 10)
            end
        end, { positions = positions, count = count, zone = zone, isRevealed = isRevealed}, timer.getTime()+delay)
    end
end

-----------------[[ END OF CommandFunctions.lua ]]-----------------



-----------------[[ JTAC.lua ]]-----------------

JTAC = {}
do
	JTAC.categories = {}
	JTAC.categories['SAM'] = {'SAM SR', 'SAM TR', 'IR Guided SAM','SAM LL','SAM CC'}
	JTAC.categories['Infantry'] = {'Infantry'}
	JTAC.categories['Armor'] = {'Tanks','IFV','APC'}
	JTAC.categories['Support'] = {'Unarmed vehicles','Artillery'}
	JTAC.categories['Structures'] = {'StaticObjects'}
	
	--{name = 'groupname'}
	function JTAC:new(obj)
		obj = obj or {}
		obj.lasers = {tgt=nil, ir=nil}
		obj.target = nil
		obj.timerReference = nil
        obj.remainingLife = nil
		obj.tgtzone = nil
		obj.priority = nil
		obj.jtacMenu = nil
		obj.laserCode = 1688
		obj.side = Group.getByName(obj.name):getCoalition()
		setmetatable(obj, self)
		self.__index = self
		obj:initCodeListener()
		return obj
	end
	
	function JTAC:initCodeListener()
		local ev = {}
		ev.context = self
		function ev:onEvent(event)
			if event.id == 26 then
				if event.text:find('^jtac%-code:') then
					local s = event.text:gsub('^jtac%-code:', '')
					local code = tonumber(s)
					self.context:setCode(code)
                    trigger.action.removeMark(event.idx)
				end
			end
		end
		
		world.addEventHandler(ev)
	end

    function JTAC:setCode(code)
        if code>=1111 and code <= 1788 then
            self.laserCode = code
            trigger.action.outTextForCoalition(self.side, 'JTAC code set to '..code, 10)
        else
            trigger.action.outTextForCoalition(self.side, 'Invalid laser code. Must be between 1111 and 1788 ', 10)
        end
    end
	
	function JTAC:showMenu()
		local gr = Group.getByName(self.name)
		if not gr then
			return
		end
		
		if not self.jtacMenu then
			self.jtacMenu = missionCommands.addSubMenuForCoalition(self.side, 'JTAC')
			
			missionCommands.addCommandForCoalition(self.side, 'Target report', self.jtacMenu, function(dr)
				if Group.getByName(dr.name) then
					dr:printTarget(true)
				else
					missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu)
					dr.jtacMenu = nil
				end
			end, self)
			
			missionCommands.addCommandForCoalition(self.side, 'Next Target', self.jtacMenu, function(dr)
				if Group.getByName(dr.name) then
					dr:searchTarget()
				else
					missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu)
					dr.jtacMenu = nil
				end
			end, self)
			
			missionCommands.addCommandForCoalition(self.side, 'Deploy Smoke', self.jtacMenu, function(dr)
				if Group.getByName(dr.name) then
					local tgtunit = Unit.getByName(dr.target)
                    if not tgtunit then
                        tgtunit = StaticObject.getByName(dr.target)
                    end

					if tgtunit then
						trigger.action.smoke(tgtunit:getPoint(), 3)
						trigger.action.outTextForCoalition(dr.side, 'JTAC target marked with ORANGE smoke', 10)
					end
				else
					missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu)
					dr.jtacMenu = nil
				end
			end, self)
			
			local priomenu = missionCommands.addSubMenuForCoalition(self.side, 'Set Priority', self.jtacMenu)
			for i,v in pairs(JTAC.categories) do
				missionCommands.addCommandForCoalition(self.side, i, priomenu, function(dr, cat)
					if Group.getByName(dr.name) then
						dr:setPriority(cat)
						dr:searchTarget()
					else
						missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu)
						dr.jtacMenu = nil
					end
				end, self, i)
			end

            local dial = missionCommands.addSubMenuForCoalition(self.side, 'Set Laser Code', self.jtacMenu)
            for i2=1,7,1 do
                local digit2 = missionCommands.addSubMenuForCoalition(self.side, '1'..i2..'__', dial)
                for i3=1,9,1 do
                    local digit3 = missionCommands.addSubMenuForCoalition(self.side, '1'..i2..i3..'_', digit2)
                    for i4=1,9,1 do
                        local digit4 = missionCommands.addSubMenuForCoalition(self.side, '1'..i2..i3..i4, digit3)
                        local code = tonumber('1'..i2..i3..i4)
                        missionCommands.addCommandForCoalition(self.side, 'Accept', digit4, Utils.log(self.setCode), self, code)
                    end
                end
            end
			
			missionCommands.addCommandForCoalition(self.side, "Clear", priomenu, function(dr)
				if Group.getByName(dr.name) then
					dr:clearPriority()
					dr:searchTarget()
				else
					missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu)
					dr.jtacMenu = nil
				end
			end, self)
		end
	end
	
	function JTAC:setPriority(prio)
		self.priority = JTAC.categories[prio]
		self.prioname = prio
	end
	
	function JTAC:clearPriority()
		self.priority = nil
	end
	
	function JTAC:setTarget(unit)
		
		if self.lasers.tgt then
			self.lasers.tgt:destroy()
			self.lasers.tgt = nil
		end
		
		if self.lasers.ir then
			self.lasers.ir:destroy()
			self.lasers.ir = nil
		end
		
		local me = Group.getByName(self.name)
		if not me then return end
		
		local pnt = unit:getPoint()
		self.lasers.tgt = Spot.createLaser(me:getUnit(1), { x = 0, y = 2.0, z = 0 }, pnt, self.laserCode)
		self.lasers.ir = Spot.createInfraRed(me:getUnit(1), { x = 0, y = 2.0, z = 0 }, pnt)
		
		self.target = unit:getName()
	end

    function JTAC:setLifeTime(minutes)
        self.remainingLife = minutes
        
        timer.scheduleFunction(function(param, time)
            if param.remainingLife == nil then return end

            local gr = Group.getByName(self.name)
		    if not gr then
                param.remainingLife = nil
                return 
            end

            param.remainingLife = param.remainingLife - 1
            if param.remainingLife < 0 then
                param:clearTarget()
                return
            end

            return time+60
        end, self, timer.getTime()+60)
    end
	
	function JTAC:printTarget(makeitlast)
		local toprint = ''
		if self.target and self.tgtzone then
			local tgtunit = Unit.getByName(self.target)
            local isStructure = false
            if not tgtunit then 
                tgtunit = StaticObject.getByName(self.target)
                isStructure = true
            end

			if tgtunit and tgtunit:isExist() then
				local pnt = tgtunit:getPoint()
                local tgttype = "Unidentified"
                if isStructure then
                    tgttype = "Structure"
                else
                    tgttype = tgtunit:getTypeName()
                end
				
				if self.priority then
					toprint = 'Priority targets: '..self.prioname..'\n'
				end
				
				toprint = toprint..'Lasing '..tgttype..' at '..self.tgtzone.name..'\nCode: '..self.laserCode..'\n'
				local lat,lon,alt = coord.LOtoLL(pnt)
				local mgrs = coord.LLtoMGRS(coord.LOtoLL(pnt))
				toprint = toprint..'\nDDM:  '.. mist.tostringLL(lat,lon,3)
				toprint = toprint..'\nDMS:  '.. mist.tostringLL(lat,lon,2,true)
				toprint = toprint..'\nMGRS: '.. mist.tostringMGRS(mgrs, 5)
				toprint = toprint..'\n\nAlt: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft'
			else
				makeitlast = false
				toprint = 'No Target'
			end
		else
			makeitlast = false
			toprint = 'No target'
		end
		
		local gr = Group.getByName(self.name)
		if makeitlast then
			trigger.action.outTextForCoalition(gr:getCoalition(), toprint, 60)
		else
			trigger.action.outTextForCoalition(gr:getCoalition(), toprint, 10)
		end
	end
	
	function JTAC:clearTarget()
		self.target = nil
	
		if self.lasers.tgt then
			self.lasers.tgt:destroy()
			self.lasers.tgt = nil
		end
		
		if self.lasers.ir then
			self.lasers.ir:destroy()
			self.lasers.ir = nil
		end
		
		if self.timerReference then
			timer.removeFunction(self.timerReference)
			self.timerReference = nil
		end
		
		local gr = Group.getByName(self.name)
		if gr then
			gr:destroy()
			missionCommands.removeItemForCoalition(self.side, self.jtacMenu)
			self.jtacMenu = nil
		end
	end
	
	function JTAC:searchTarget()
		local gr = Group.getByName(self.name)
		if gr and gr:isExist() then
			if self.tgtzone and self.tgtzone.side~=0 and self.tgtzone.side~=gr:getCoalition() then
				local viabletgts = {}
				for i,v in pairs(self.tgtzone.built) do
					local tgtgr = Group.getByName(v.name)
					if tgtgr and tgtgr:isExist() and tgtgr:getSize()>0 then
						for i2,v2 in ipairs(tgtgr:getUnits()) do
							if v2:isExist() and v2:getLife()>=1 then
								table.insert(viabletgts, v2)
							end
						end
                    else
                        tgtgr = StaticObject.getByName(v.name)
                        if tgtgr and tgtgr:isExist() then
                            table.insert(viabletgts, tgtgr)
                        end
                    end
				end
				
				if self.priority then
					local priorityTargets = {}
					for i,v in ipairs(viabletgts) do
						for i2,v2 in ipairs(self.priority) do
                            if v2 == "StaticObjects" and ZoneCommand.staticRegistry[v:getName()] then
                                table.insert(priorityTargets, v)
                                break
                            elseif v:hasAttribute(v2) and v:getLife()>=1 then
								table.insert(priorityTargets, v)
								break
							end
						end
					end
					
					if #priorityTargets>0 then
						viabletgts = priorityTargets
					else
						self:clearPriority()
						trigger.action.outTextForCoalition(gr:getCoalition(), 'JTAC: No priority targets found', 10)
					end
				end
				
				if #viabletgts>0 then
					local chosentgt = math.random(1, #viabletgts)
					self:setTarget(viabletgts[chosentgt])
					self:printTarget()
				else
					self:clearTarget()
				end
			else
				self:clearTarget()
			end
		end
	end
	
	function JTAC:searchIfNoTarget()
		if Group.getByName(self.name) then
			if not self.target then
				self:searchTarget()
			else
				local un = Unit.getByName(self.target)
				if un and un:isExist() then
					if un:getLife()>=1 then
						self:setTarget(un)
					else
						self:searchTarget()
					end
				else
                    local st = StaticObject.getByName(self.target)
                    if st and st:isExist() then
                        self:setTarget(st)
					else
						self:searchTarget()
					end
                end
			end
		else
			self:clearTarget()
		end
	end
	
	function JTAC:deployAtZone(zoneCom)
        self.remainingLife = nil
		self.tgtzone = zoneCom
		local p = CustomZone:getByName(self.tgtzone.name).point
		local vars = {}
		vars.gpName = self.name
		vars.action = 'respawn' 
		vars.point = {x=p.x, y=5000, z = p.z}
		mist.teleportToPoint(vars)
		
		timer.scheduleFunction(function(param,time)
			param.context:setOrbit(param.target, param.point)
		end, {context = self, target = self.tgtzone.zone, point = p}, timer.getTime()+1)
		
		if not self.timerReference then
			self.timerReference = timer.scheduleFunction(function(param, time)
				param:searchIfNoTarget()
				return time+5
			end, self, timer.getTime()+5)
		end
	end
	
	function JTAC:setOrbit(zonename, point)
		local gr = Group.getByName(self.name)
		if not gr then 
			return
		end
		
		local cnt = gr:getController()
		cnt:setCommand({ 
			id = 'SetInvisible', 
			params = { 
				value = true 
			} 
		})
  
		cnt:setTask({ 
			id = 'Orbit', 
			params = { 
				pattern = 'Circle',
				point = {x = point.x, y=point.z},
				altitude = 5000
			} 
		})
		
		self:searchTarget()
	end
end

-----------------[[ END OF JTAC.lua ]]-----------------



-----------------[[ CarrierMap.lua ]]-----------------

CarrierMap = {}
do
	CarrierMap.currentIndex = 15000
    function CarrierMap:new(zoneList)

        local obj = {}
        obj.zones = {}

        for i,v in ipairs(zoneList) do
            local zn = CustomZone:getByName(v)

            local id = CarrierMap.currentIndex
            CarrierMap.currentIndex = CarrierMap.currentIndex + 1
            
            zn:draw(id, {1,1,1,0.2}, {1,1,1,0.2})
            obj.zones[v] = {zone = zn, waypoints = {}}

            for subi=1,1000,1 do
                local subname = v..'-'..subi
                if CustomZone:getByName(subname) then
                    table.insert(obj.zones[v].waypoints, subname)
                else
                    break
                end
            end

            id = CarrierMap.currentIndex
            CarrierMap.currentIndex = CarrierMap.currentIndex + 1

            trigger.action.textToAll(-1, id , zn.point, {0,0,0,0.8}, {1,1,1,0}, 15, true, v)
            for i,wps in ipairs(obj.zones[v].waypoints) do
                id = CarrierMap.currentIndex
                CarrierMap.currentIndex = CarrierMap.currentIndex + 1
                local point = CustomZone:getByName(wps).point
                trigger.action.textToAll(-1, id, point, {0,0,0,0.8}, {1,1,1,0}, 10, true, wps)
            end
        end

        setmetatable(obj, self)
		self.__index = self

        return obj
    end

    function CarrierMap:getNavMap()
        local map = {}
        for nm, zn in pairs(self.zones) do
            table.insert(map, {name = zn.zone.name, waypoints = zn.waypoints})
        end

        table.sort(map, function(a,b) return a.name < b.name end)
        return map
    end
end

-----------------[[ END OF CarrierMap.lua ]]-----------------



-----------------[[ CarrierCommand.lua ]]-----------------

CarrierCommand = {}
do
    CarrierCommand.allCarriers = {}
	CarrierCommand.currentIndex = 6000
    CarrierCommand.isCarrier = true

	CarrierCommand.supportTypes = {
		strike = 'Strike',
		cap = 'CAP',
		awacs = 'AWACS',
		tanker = 'Tanker',
		transport = 'Transport',
		mslstrike = 'Cruise Missiles'
	}

	CarrierCommand.supportStates = {
		takeoff = 'takeoff',
		inair = 'inair',
		landed = 'landed',
		none = 'none'
	}

	CarrierCommand.blockedDespawnTime = 10*60
	CarrierCommand.recoveryReduction = 0.8
	CarrierCommand.landedDespawnTime = 10
    
    function CarrierCommand:new(name, range, navmap, radioConfig, maxResource)
        local unit = Unit.getByName(name)
        if not unit then return end

        local obj = {}
        obj.name = name
        obj.range = range
        obj.side = unit:getCoalition()
        obj.resource = maxResource or 30000
        obj.maxResource = maxResource or 30000
		obj.spendTreshold = 500
        obj.revealTime = 0
        obj.isHeloSpawn = true
        obj.isPlaneSpawn = true
		obj.supportFlights = {}
		obj.extraSupports = {}
		obj.weaponStocks = {}
		
		obj.navigation = {
			currentWaypoint = nil,
			waypoints = {},
			loop = true
		}

		obj.navmap = navmap
        
        obj.tacan = radioConfig.tacan
        obj.icls = radioConfig.icls
        obj.acls = radioConfig.acls
        obj.link4 = radioConfig.link4
        obj.radio = radioConfig.radio

        obj.spawns = {}
		for i,v in pairs(mist.DBs.groupsByName) do
			if v.units[1].skill == 'Client' then
				local pos3d = {
					x = v.units[1].point.x,
					y = 0,
					z = v.units[1].point.y
				}
				
				if Utils.isInCircle(pos3d, unit:getPoint(), obj.range)then
					table.insert(obj.spawns, {name=i})
				end
			end
		end

        obj.index = CarrierCommand.currentIndex
		CarrierCommand.currentIndex = CarrierCommand.currentIndex + 1

        local point = unit:getPoint()

        local color = {0.7,0.7,0.7,0.3}
		if obj.side == 1 then
			color = {1,0,0,0.3}
		elseif obj.side == 2 then
			color = {0,0,1,0.3}
		end

        trigger.action.circleToAll(-1,3000+obj.index,point, obj.range, color, color, 1)
        
        point.z = point.z + obj.range
        trigger.action.textToAll(-1,2000+obj.index, point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '')
		
        setmetatable(obj, self)
		self.__index = self

        obj:start()
        obj:refreshText()
        obj:refreshSpawnBlocking()
		CarrierCommand.allCarriers[obj.name] = obj
        return obj
    end

	function CarrierCommand:setupRadios()
		local unit = Unit.getByName(self.name)
		TaskExtensions.setupCarrier(unit, self.icls, self.acls, self.tacan, self.link4, self.radio)
	end

    function CarrierCommand:start()
		self:setupRadios()

        timer.scheduleFunction(function(param, time)
            local self = param.context
            local unit = Unit.getByName(self.name)
            if not unit then
				self:clearDrawings()
				local gr = Group.getByName(self.name)
				if gr and gr:isExist() then
					TaskExtensions.stopCarrier(gr)
				end
				return
			end
			
			self:updateNavigation()
			self:updateSupports()
            self:refreshText()
            return time+10
        end, {context = self}, timer.getTime()+1)
    end

	function CarrierCommand:clearDrawings()
		if not self.cleared then
			trigger.action.removeMark(2000+self.index)
			trigger.action.removeMark(3000+self.index)
			self.cleared = true
		end
	end

	function CarrierCommand:updateSupports()
		for _, data in pairs(self.supportFlights) do
			self:processAir(data)
		end

		
		for wep, stock in pairs(self.weaponStocks) do
			local gr = Unit.getByName(self.name):getGroup()
			local realstock = Utils.getAmmo(gr, wep)
			self.weaponStocks[wep] = math.min(stock, realstock)	
		end
	end

	local function setState(group, state)
		group.state = state
		group.lastStateTime = timer.getAbsTime()
	end

	local function isAttack(group)
		if group.type == CarrierCommand.supportTypes.cap then return true end
		if group.type == CarrierCommand.supportTypes.strike then return true end
	end

	local function hasWeapons(group)
		for _,un in ipairs(group:getUnits()) do
			local wps = un:getAmmo()
			if wps then
				for _,w in ipairs(wps) do
					if w.desc.category ~= 0 and w.count > 0 then
						return true
					end
				end
			end
		end
	end

	function CarrierCommand:processAir(group)
		local carrier = Unit.getByName(self.name)
		if not carrier or not carrier:isExist() then return end

		local gr = Group.getByName(group.name)
		if not gr or not gr:isExist() then
			if group.state ~= CarrierCommand.supportStates.none then
				setState(group, CarrierCommand.supportStates.none)
				group.returning = false
				env.info('CarrierCommand: processAir ['..group.name..'] does not exist state=none')
			end
			return
		end
		
		if gr:getSize() == 0 then
			gr:destroy()
			setState(group, CarrierCommand.supportStates.none)
			group.returning = false
			env.info('CarrierCommand: processAir ['..group.name..'] has no members state=none')
			return
		end

		if group.state == CarrierCommand.supportStates.none then
			setState(group, CarrierCommand.supportStates.takeoff)
			env.info('CarrierCommand: processAir ['..group.name..'] started existing state=takeoff')
		elseif group.state == CarrierCommand.supportStates.takeoff then
			if timer.getAbsTime() - group.lastStateTime > CarrierCommand.blockedDespawnTime then
				if gr and gr:getSize()>0 and gr:getUnit(1):isExist() then
					local frUnit = gr:getUnit(1)
					local cz = CarrierCommand.getCarrierOfUnit(frUnit:getName())
					if Utils.allGroupIsLanded(gr, cz ~= nil) then
						env.info('CarrierCommand: processAir ['..group.name..'] is blocked, despawning')
						local frUnit = gr:getUnit(1)
						if frUnit then
							local firstUnit = frUnit:getName()
							local z = ZoneCommand.getZoneOfUnit(firstUnit)
							if not z then 
								z = CarrierCommand.getCarrierOfUnit(firstUnit)
							end
							if z then
								z:addResource(group.cost)
								env.info('CarrierCommand: processAir ['..z.name..'] has recovered ['..group.cost..'] from ['..group.name..']')
							end
						end

						gr:destroy()
						setState(group, CarrierCommand.supportStates.none)
						group.returning = false
						env.info('CarrierCommand: processAir ['..group.name..'] has been removed due to being blocked state=none')
						return
					end
				end
			elseif gr and Utils.someOfGroupInAir(gr) then
				env.info('CarrierCommand: processAir ['..group.name..'] is in the air state=inair')
				setState(group, CarrierCommand.supportStates.inair)
			end
		elseif group.state == CarrierCommand.supportStates.inair then
			if gr and gr:getSize()>0 and gr:getUnit(1) and gr:getUnit(1):isExist() then
				local frUnit = gr:getUnit(1)
				local cz = CarrierCommand.getCarrierOfUnit(frUnit:getName())
				if Utils.allGroupIsLanded(gr, cz ~= nil) then
					env.info('CarrierCommand: processAir ['..group.name..'] has landed state=landed')
					setState(group, CarrierCommand.supportStates.landed)

					local unit = gr:getUnit(1)
					if unit then
						local firstUnit = unit:getName()
						local z = ZoneCommand.getZoneOfUnit(firstUnit)
						if not z then 
							z = CarrierCommand.getCarrierOfUnit(firstUnit)
						end
						
						if group.type == CarrierCommand.supportTypes.transport then
							if z then
								z:capture(gr:getCoalition())
								z:addResource(group.cost)
								env.info('CarrierCommand: processAir ['..group.name..'] has supplied ['..z.name..'] with ['..group.cost..']')
							end
						else
							if z and z.side == gr:getCoalition() then
								local percentSurvived = gr:getSize()/gr:getInitialSize()
								local torecover = math.floor(group.cost * percentSurvived * CarrierCommand.recoveryReduction)
								z:addResource(torecover)
								env.info('CarrierCommand: processAir ['..z.name..'] has recovered ['..torecover..'] from ['..group.name..']')
							end
						end
					else
						env.info('CarrierCommand: processAir ['..group.name..'] size ['..gr:getSize()..'] has no unit 1')
					end
				else
					if isAttack(group) and not group.returning then
						if not hasWeapons(gr) then
							env.info('CarrierCommand: processAir ['..group.name..'] size ['..gr:getSize()..'] has no weapons outside of shells')
							group.returning = true

							local point = carrier:getPoint()
							TaskExtensions.landAtAirfield(gr, {x=point.x, y=point.z})
							local cnt = gr:getController()
							cnt:setOption(0,4) -- force ai hold fire
							cnt:setOption(1, 4) -- force reaction on threat to allow abort
						end
					elseif group.type == CarrierCommand.supportTypes.transport then
						if not group.returning and group.target and group.target.side ~= self.side and group.target.side ~= 0 then
							group.returning = true
							local point = carrier:getPoint()
							TaskExtensions.landAtPointFromAir(gr,  {x=point.x, y=point.z}, group.altitude)
							env.info('CarrierCommand: processAir ['..group.name..'] returning home due to invalid target')
						end
					end
				end
			end
		elseif group.state == CarrierCommand.supportStates.landed then
			if timer.getAbsTime() - group.lastStateTime > CarrierCommand.landedDespawnTime then
				if gr then
					gr:destroy()
					setState(group, CarrierCommand.supportStates.none)
					group.returning = false
					env.info('CarrierCommand: processAir ['..group.name..'] despawned after landing state=none')
					return true
				end
			end
		end
	end

	function CarrierCommand:setWaypoints(wplist)
		self.navigation.waypoints = wplist
		self.navigation.currentWaypoint = nil
		self.navigation.nextWaypoint = 1
		self.navigation.loop = #wplist > 1
	end

	function CarrierCommand:updateNavigation()
		local unit = Unit.getByName(self.name)

		if self.navigation.nextWaypoint then
			local dist = 0
			if self.navigation.currentWaypoint then
				local tgzn = self.navigation.waypoints[self.navigation.currentWaypoint]
				local point = CustomZone:getByName(tgzn).point
				dist = mist.utils.get2DDist(unit:getPoint(), point)
			end

			if dist<2000 then
				self.navigation.currentWaypoint = self.navigation.nextWaypoint

				local tgzn = self.navigation.waypoints[self.navigation.currentWaypoint]
				local point = CustomZone:getByName(tgzn).point
				env.info("CarrierCommand - sending "..self.name.." to "..tgzn.." x"..point.x.." z"..point.z)
				TaskExtensions.carrierGoToPos(unit:getGroup(), point)

				if self.navigation.loop then
					self.navigation.nextWaypoint = self.navigation.nextWaypoint + 1
					if self.navigation.nextWaypoint > #self.navigation.waypoints then
						self.navigation.nextWaypoint = 1
					end
				else
					self.navigation.nextWaypoint = nil
				end
			end
		else
			local dist = 9999999
			if self.navigation.currentWaypoint then
				local tgzn = self.navigation.waypoints[self.navigation.currentWaypoint]
				local point = CustomZone:getByName(tgzn).point
				dist = mist.utils.get2DDist(unit:getPoint(), point)
			end

			if dist<2000 then
				env.info("CarrierCommand - "..self.name.." stopping after reached waypoint")
				TaskExtensions.stopCarrier(unit:getGroup())
				self.navigation.currentWaypoint = nil
			end
		end
	end

	function CarrierCommand:addSupportFlight(name, cost, type, data)
		self.supportFlights[name] = { 
			name = name, 
			cost = cost, 
			type = type, 
			target = nil,
			state = CarrierCommand.supportStates.none, 
			lastStateTime = timer.getAbsTime(),
			carrier = self
		}

		for i,v in pairs(data) do
			self.supportFlights[name][i] = v
		end

		local gr = Group.getByName(name)
		if gr then gr:destroy() end
	end

	function CarrierCommand:addExtraSupport(name, cost, type, data)
		self.extraSupports[name] = { 
			name = name, 
			cost = cost, 
			type = type, 
			target = nil,
			carrier = self
		}

		for i,v in pairs(data) do
			self.extraSupports[name][i] = v
		end
	end

	function CarrierCommand:setWPStock(wpname, amount)
		self.weaponStocks[wpname] = amount
	end

	function CarrierCommand:callExtraSupport(data, groupname)
		local playerGroup = Group.getByName(groupname)
		if not playerGroup then return end

		if self.resource < data.cost then
			trigger.action.outTextForGroup(playerGroup:getID(), self.name..' does not have enough resources for '..data.name, 10)
			return
		end

		local cru = Unit.getByName(self.name)
		if not cru or not cru:isExist() then return end

		local crg = cru:getGroup()

		local ammo = self.weaponStocks[data.wpType] or 0
		if ammo < data.salvo then
			trigger.action.outTextForGroup(playerGroup:getID(), data.name..' is not available at this time.', 10)
			return
		end

		local success = MenuRegistry.showTargetZoneMenu(playerGroup:getID(), "Select "..data.name..'('..data.type..") target", function(params)
			local cru = Unit.getByName(params.data.carrier.name)
			if not cru or not cru:isExist() then return end
			local crg = cru:getGroup()

			TaskExtensions.fireAtTargets(crg, params.zone.built, params.data.salvo)
			if params.data.carrier.weaponStocks[params.data.wpType] then
				params.data.carrier.weaponStocks[params.data.wpType] = params.data.carrier.weaponStocks[params.data.wpType] - params.data.salvo
			end
		end, 1, nil, data, nil, true)

		if success then
			self:removeResource(data.cost)
			trigger.action.outTextForGroup(playerGroup:getID(), 'Select target for '..data.name..' ('..data.type..') from radio menu.', 10)
		else
			trigger.action.outTextForGroup(playerGroup:getID(), 'No valid targets for '..data.name..' ('..data.type..')', 10)
		end
	end

	function CarrierCommand:callSupport(data, groupname)
		local playerGroup = Group.getByName(groupname)
		if not playerGroup then return end

		if Group.getByName(data.name) and (timer.getAbsTime() - data.lastStateTime < 60*60) then 
			trigger.action.outTextForGroup(playerGroup:getID(), data.name..' tasking is not available at this time.', 10)
			return
		end

		if self.resource < data.cost then
			trigger.action.outTextForGroup(playerGroup:getID(), self.name..' does not have enough resources to deploy '..data.name, 10)
			return
		end
		
		local targetCoalition = nil
		local minDistToFront = nil
		local includeCarriers = nil
		local onlyRevealed = nil

		if data.type == CarrierCommand.supportTypes.strike then
			targetCoalition = 1
			onlyRevealed = true
		elseif data.type == CarrierCommand.supportTypes.cap then
			minDistToFront = 1
			includeCarriers = true
		elseif data.type == CarrierCommand.supportTypes.awacs then
			targetCoalition = 2
			includeCarriers = true
		elseif data.type == CarrierCommand.supportTypes.tanker then
			targetCoalition = 2
			includeCarriers = true
		elseif data.type == CarrierCommand.supportTypes.transport then
			targetCoalition = {0,2}
		end
		
		local success = MenuRegistry.showTargetZoneMenu(playerGroup:getID(), "Select "..data.name..'('..data.type..") target", function(params)
			CarrierCommand.spawnSupport(params.data, params.zone)
			trigger.action.outTextForGroup(params.groupid, params.data.name..'('..params.data.type..') heading to '..params.zone.name, 10)
		end, targetCoalition, minDistToFront, data, includeCarriers, onlyRevealed)

		if success then
			self:removeResource(data.cost)
			trigger.action.outTextForGroup(playerGroup:getID(), 'Select target for '..data.name..' ('..data.type..') from radio menu.', 10)
		else
			trigger.action.outTextForGroup(playerGroup:getID(), 'No valid targets for '..data.name..' ('..data.type..')', 10)
		end
	end

	local function getDefaultPos(savedData)
		local action = 'Turning Point'
		local speed = 250

		local vars = {
			groupName = savedData.name,
			point = savedData.position,
			action = 'respawn',
			heading = savedData.heading,
			initTasks = false,
			route = { 
				[1] = {
					alt = savedData.position.y,
					type = 'Turning Point',
					action = action,
					alt_type = 'BARO',
					x = savedData.position.x,
					y = savedData.position.z,
					speed = speed
				}
			}
		}

		return vars
	end

	function CarrierCommand.spawnSupport(data, target, saveData)
		data.target = target

		if saveData then
			mist.teleportToPoint(getDefaultPos(saveData))
			data.state = saveData.state
			data.lastStateTime = timer.getAbsTime() - saveData.lastStateDuration
			data.returning = saveData.returning
		else
			mist.respawnGroup(data.name, true)
		end

		if data.type == CarrierCommand.supportTypes.strike then
			CarrierCommand.dispatchStrike(data, saveData~=nil)
		elseif data.type == CarrierCommand.supportTypes.cap then
			CarrierCommand.dispatchCap(data, saveData~=nil)
		elseif data.type == CarrierCommand.supportTypes.awacs then
			CarrierCommand.dispatchAwacs(data, saveData~=nil)
		elseif data.type == CarrierCommand.supportTypes.tanker then
			CarrierCommand.dispatchTanker(data, saveData~=nil)
		elseif data.type == CarrierCommand.supportTypes.transport then
			CarrierCommand.dispatchTransport(data, saveData~=nil)
		end
	end

	function CarrierCommand.dispatchStrike(data, isReactivated)
		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.data.name)
			local homePos = nil
			local carrier = Unit.getByName(param.data.carrier.name)
			if carrier and isReactivated then
				homePos = { homePos = carrier:getPoint() }
			end
			env.info('CarrierCommand - sending '..param.data.name..' to '..param.data.target.name)
			
			local targets = {}
			for i,v in pairs(param.data.target.built) do
				if v.type == 'upgrade' and v.side ~= gr:getCoalition() then
					local tg = TaskExtensions.getTargetPos(v.name)
					table.insert(targets, tg)
				end
			end

			if #targets == 0 then 
				gr:destroy()
				return 
			end

			local choice = targets[math.random(1, #targets)]
			TaskExtensions.executePinpointStrikeMission(gr, choice, AI.Task.WeaponExpend.ALL, param.data.altitude, homePos, carrier:getID())
		end, {data = data}, timer.getTime()+1)
	end

	function CarrierCommand.dispatchCap(data, isReactivated)
		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.data.name)

			local homePos = nil
			local carrier = Unit.getByName(param.data.carrier.name)
			if carrier and isReactivated  then
				homePos = { homePos = carrier:getPoint() }
			end

			local point = nil
			if param.data.target.isCarrier then
				point = Unit.getByName(param.data.target.name):getPoint()
			else
				point = trigger.misc.getZone(param.data.target.name).point
			end

			TaskExtensions.executePatrolMission(gr, point, param.data.altitude, param.data.range, homePos, carrier:getID())
		end, {data = data}, timer.getTime()+1)
	end

	function CarrierCommand.dispatchAwacs(data, isReactivated)		
		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.data.name)

			local homePos = nil
			local carrier = Unit.getByName(param.data.carrier.name)
			if carrier and isReactivated  then
				homePos = { homePos = carrier:getPoint() }
			end
			
			local un = gr:getUnit(1)
			if un then 
				local callsign = un:getCallsign()
				RadioFrequencyTracker.registerRadio(param.data.name, '[AWACS] '..callsign, param.data.freq..' AM')
			end

			local point = nil
			if param.data.target.isCarrier then
				point = Unit.getByName(param.data.target.name):getPoint()
			else
				point = trigger.misc.getZone(param.data.target.name).point
			end

			TaskExtensions.executeAwacsMission(gr, point, param.data.altitude, param.data.freq, homePos, carrier:getID())
		end, {data = data}, timer.getTime()+1)
	end

	function CarrierCommand.dispatchTanker(data, isReactivated)
		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.data.name)

			local homePos = nil
			local carrier = Unit.getByName(param.data.carrier.name)
			if carrier and isReactivated  then
				homePos = { homePos = carrier:getPoint() }
			end

			local un = gr:getUnit(1)
			if un then 
				local callsign = un:getCallsign()
				RadioFrequencyTracker.registerRadio(param.data.name, '[Tanker(Drogue)] '..callsign, param.data.freq..' AM | TCN '..param.data.tacan..'X')
			end
			
			local point = nil
			if param.data.target.isCarrier then
				point = Unit.getByName(param.data.target.name):getPoint()
			else
				point = trigger.misc.getZone(param.data.target.name).point
			end

			TaskExtensions.executeTankerMission(gr, point, param.data.altitude, param.data.freq, param.data.tacan, homePos, carrier:getID())
		end, {data = data}, timer.getTime()+1)
	end

	function CarrierCommand.dispatchTransport(data, isReactivated)
		timer.scheduleFunction(function(param)
			local gr = Group.getByName(param.data.name)

			local supplyPoint = trigger.misc.getZone(param.data.target.name..'-hsp')
			if not supplyPoint then
				supplyPoint = trigger.misc.getZone(param.data.target.name)
			end
			
			local point = { x=supplyPoint.point.x, y = supplyPoint.point.z}
			TaskExtensions.landAtPoint(gr, point, param.data.altitude, true)
		end, {data = data}, timer.getTime()+1)
	end

	function CarrierCommand:showInformation(groupname)
		local gr = Group.getByName(groupname)
        if gr then 
			local msg = '['..self.name..']'
			if self.radio then msg = msg..'\n Radio: '..string.format('%.3f',self.radio/1000000)..' AM' end
			if self.tacan then msg = msg..'\n TACAN: '..self.tacan.channel..'X ('..self.tacan.callsign..')' end
			if self.link4 then msg = msg..'\n Link4: '..string.format('%.3f',self.link4/1000000) end
			if self.icls then msg = msg..'\n ICLS: '..self.icls end

			if Utils.getTableSize(self.supportFlights) > 0 then
				local flights = {}
				for _, data in pairs(self.supportFlights) do
					if (data.state == CarrierCommand.supportStates.none or (timer.getAbsTime()-data.lastStateTime >= 60*60)) and data.cost <= self.resource then
						table.insert(flights, data)
					end
				end
				
				table.sort(flights, function(a,b) return a.name<b.name end)
				
				if #flights > 0 then
					msg = msg..'\n\n Available for tasking:'
					for _,data in ipairs(flights) do
						msg = msg..'\n    '..data.name..' ('..data.type..') ['..data.cost..']'
					end
				end
			end

			if Utils.getTableSize(self.extraSupports) > 0 then
				local extras = {}
				for _, data in pairs(self.extraSupports) do
					if data.cost <= self.resource then
						if data.type == CarrierCommand.supportTypes.mslstrike then
							local cru = Unit.getByName(self.name)
							if cru and cru:isExist() then
								local crg = cru:getGroup()
								local remaining = self.weaponStocks[data.wpType] or 0
								if remaining > data.salvo then
									table.insert(extras, data)
								end
							end
						end
					end
				end
				
				table.sort(extras, function(a,b) return a.name<b.name end)
				
				if #extras > 0 then
					msg = msg..'\n\n Other:'
					for _,data in ipairs(extras) do
						if data.type == CarrierCommand.supportTypes.mslstrike then
							local cru = Unit.getByName(self.name)
							if cru and cru:isExist() then
								local crg = cru:getGroup()
								local remaining = self.weaponStocks[data.wpType] or 0
								if remaining > data.salvo then
									remaining = math.floor(remaining/data.salvo)
									msg = msg..'\n    '..data.name..' ('..data.type..') ['..data.cost..'] ('..remaining..' left)'
								end
							end
						end
					end
				end
			end

            trigger.action.outTextForGroup(gr:getID(), msg, 20)
		end
	end

    function CarrierCommand:addResource(amount)
		self.resource = self.resource+amount
		self.resource = math.floor(math.min(self.resource, self.maxResource))
        self:refreshSpawnBlocking()
        self:refreshText()
    end

    function CarrierCommand:removeResource(amount)
		self.resource = self.resource-amount
		self.resource = math.floor(math.max(self.resource, 0))
        self:refreshSpawnBlocking()
        self:refreshText()
    end

    function CarrierCommand:refreshSpawnBlocking()
		for _,v in ipairs(self.spawns) do
			trigger.action.setUserFlag(v.name, self.resource < Config.carrierSpawnCost)
		end
	end

    function CarrierCommand:refreshText()		
        local build = ''
		local mBuild = ''
		
		local status=''
		if self:criticalOnSupplies() then
			status = '(!)'
		end

		local color = {0.3,0.3,0.3,1}
		if self.side == 1 then
			color = {0.7,0,0,1}
		elseif self.side == 2 then
			color = {0,0,0.7,1}
		end

		trigger.action.setMarkupColor(2000+self.index, color)

		local label = '['..self.resource..'/'..self.maxResource..']'..status..build..mBuild

		if self.side == 1 then
			if self.revealTime > 0 then
				trigger.action.setMarkupText(2000+self.index, self.name..label)
			else
				trigger.action.setMarkupText(2000+self.index, self.name)
			end
		elseif self.side == 2 then
			trigger.action.setMarkupText(2000+self.index, self.name..label)
		elseif self.side == 0 then
			trigger.action.setMarkupText(2000+self.index, ' '..self.name..' ')
		end

        if self.side == 2 and (self.isHeloSpawn or self.isPlaneSpawn) then
			trigger.action.setMarkupTypeLine(3000+self.index, 2)
			trigger.action.setMarkupColor(3000+self.index, {0,1,0,1})
		end

        local unit = Unit.getByName(self.name)
        local point = unit:getPoint()
        trigger.action.setMarkupPositionStart(3000+self.index, point)

        point.z = point.z + self.range
        trigger.action.setMarkupPositionStart(2000+self.index, point)
    end

    function CarrierCommand:capture(side)
    end

    function CarrierCommand:criticalOnSupplies()
		return self.resource<=self.spendTreshold
    end

    function CarrierCommand.getCarrierByName(name)
		if not name then return nil end
		return CarrierCommand.allCarriers[name]
	end
	
	function CarrierCommand.getAllCarriers()
		return CarrierCommand.allCarriers
	end
	
	function CarrierCommand.getCarrierOfUnit(unitname)
		local un = Unit.getByName(unitname)
		
		if not un then 
			return nil
		end
		
		for i,v in pairs(CarrierCommand.allCarriers) do
            local carrier = Unit.getByName(v.name)
            if carrier then
                if Utils.isInCircle(un:getPoint(), carrier:getPoint(), v.range) then
                    return v
                end
            end
		end
		
		return nil
	end

	function CarrierCommand.getClosestCarrierToPoint(point)
		local minDist = 9999999
		local closest = nil
		for i,v in pairs(CarrierCommand.allCarriers) do
            local carrier = Unit.getByName(v.name)
            if carrier then
                local d = mist.utils.get2DDist(carrier:getPoint(), point)
                if d < minDist then
                    minDist = d
                    closest = v
                end
            end
		end
		
		return closest, minDist
	end
	
	function CarrierCommand.getCarrierOfPoint(point)
		for i,v in pairs(CarrierCommand.allCarriers) do
			local carrier = Unit.getByName(v.name)
            if carrier then
                if Utils.isInCircle(point, carrier:getPoint(), v.range) then
                    return v
                end
            end
		end
		
		return nil
	end

	CarrierCommand.groupMenus = {}
	MenuRegistry:register(6, function(event, context)
		if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
			local player = event.initiator:getPlayerName()
			if player then
				local groupid = event.initiator:getGroup():getID()
				local groupname = event.initiator:getGroup():getName()
				
				if CarrierCommand.groupMenus[groupid] then
					missionCommands.removeItemForGroup(groupid, CarrierCommand.groupMenus[groupid])
					CarrierCommand.groupMenus[groupid] = nil
				end

				if not CarrierCommand.groupMenus[groupid] then
					
					local menu = missionCommands.addSubMenuForGroup(groupid, 'Naval Command')

					local sorted = {}
					for cname, carrier in pairs(CarrierCommand.getAllCarriers()) do
						local cr = Unit.getByName(carrier.name)
						if cr then
							table.insert(sorted, carrier)
						end
					end

					table.sort(sorted, function(a,b) return a.name < b.name end)

					for _,carrier in ipairs(sorted) do
						local crunit =  Unit.getByName(carrier.name)
						if crunit and crunit:isExist() then
							local subm = missionCommands.addSubMenuForGroup(groupid, carrier.name, menu)
							missionCommands.addCommandForGroup(groupid, 'Information', subm, Utils.log(carrier.showInformation), carrier, groupname)

							local rank =  DependencyManager.get("PlayerTracker"):getPlayerRank(player)

							if rank and rank.allowCarrierSupport and Utils.getTableSize(carrier.supportFlights) > 0 then
								local supm = missionCommands.addSubMenuForGroup(groupid, "Support", subm)
								local flights = {}
								for _, data in pairs(carrier.supportFlights) do
									table.insert(flights, data)
								end

								table.sort(flights, function(a,b) return a.name<b.name end)

								for _, data in ipairs(flights) do
									local name = data.name..' ('..data.type..') ['..data.cost..']'
									missionCommands.addCommandForGroup(groupid, name, supm, Utils.log(carrier.callSupport), carrier, data, groupname)
								end

								local extras = {}
								for _, data in pairs(carrier.extraSupports) do
									table.insert(extras, data)
								end

								table.sort(extras, function(a,b) return a.name<b.name end)
								for _, data in ipairs(extras) do
									local name = data.name..' ('..data.type..') ['..data.cost..']'
									missionCommands.addCommandForGroup(groupid, name, supm, Utils.log(carrier.callExtraSupport), carrier, data, groupname)
								end
							end

							if rank and rank.allowCarrierCommand then
								local navm = missionCommands.addSubMenuForGroup(groupid, "Navigation", subm)
								for _,wp in ipairs(carrier.navmap) do
									local wpm = missionCommands.addSubMenuForGroup(groupid, wp.name, navm)
									if #wp.waypoints > 1 then
										missionCommands.addCommandForGroup(groupid, 'Patrol Area', wpm, Utils.log(carrier.setWaypoints), carrier, wp.waypoints, groupname)
									end
									
									missionCommands.addCommandForGroup(groupid, 'Go to '..wp.name, wpm, Utils.log(carrier.setWaypoints), carrier, {wp.name}, groupname)
									for _,subwp in ipairs(wp.waypoints) do
										missionCommands.addCommandForGroup(groupid, 'Go to '..subwp, wpm, Utils.log(carrier.setWaypoints), carrier, {subwp}, groupname)
									end
								end
							end
						end
					end

					CarrierCommand.groupMenus[groupid] = menu
				end
			end
		elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then
			local player = event.initiator:getPlayerName()
			if player then
				local groupid = event.initiator:getGroup():getID()
				
				if CarrierCommand.groupMenus[groupid] then
					missionCommands.removeItemForGroup(groupid, CarrierCommand.groupMenus[groupid])
					CarrierCommand.groupMenus[groupid] = nil
				end
			end
		end
	end, nil)
end

-----------------[[ END OF CarrierCommand.lua ]]-----------------



-----------------[[ Objectives/Objective.lua ]]-----------------

Objective = {}

do
    Objective.types = {
        fly_to_zone_seq = 'fly_to_zone_seq',            -- any of playerlist inside [zone] in sequence
        recon_zone = 'recon_zone',                           -- within X km, facing Y angle +-, % of enemy units in LOS progress faster
        destroy_attr = 'destroy_attr',                  -- any of playerlist kill event on target with any of [attribute]
        destroy_attr_at_zone = 'destroy_attr_at_zone',  -- any of playerlist kill event on target at [zone] with any of [attribute]
        clear_attr_at_zone = 'clear_attr_at_zone',      -- [zone] does not have any units with [attribute]
        destroy_structure = 'destroy_structure',        -- [structure] is killed by any player (getDesc().displayName or getDesc().typeName:gsub('%.','') must match)
        destroy_group = 'destroy_group',                -- [group] is missing from mission AND any player killed unit from group at least once
        supply = 'supply',                              -- any of playerlist unload [amount] supply at [zone]
        extract_pilot = 'extract_pilot',                  -- players extracted specific ejected pilots
        extract_squad = 'extract_squad',                  -- players extracted specific squad
        unloaded_pilot_or_squad = 'unloaded_pilot_or_squad', -- unloaded pilot or squad
        deploy_squad = 'deploy_squad',                  --deploy squad at zone
        escort = 'escort',                              -- escort convoy
        protect = 'protect',                            -- protect other mission
        air_kill_bonus = 'air_kill_bonus',               -- award bonus for air kills
        bomb_in_zone = 'bomb_in_zone',                     -- bombs tallied inside zone
        player_close_to_zone = 'player_close_to_zone' -- player is close to point
    }

    function Objective:new(type)

		local obj = {
            type = type,
            mission = nil,
            param = {},
            isComplete = false,
            isFailed = false
        }

		setmetatable(obj, self)
		self.__index = self

		return obj
    end

    function Objective:initialize(mission, param)
        self.mission = mission
        self:validateParameters(param)
        self.param = param
    end

    function Objective:getType()
        return self.type
    end

    function Objective:validateParameters(param)
        for i,v in pairs(self.requiredParams) do
            if v and param[i] == nil then
                env.error("Objective - missing parameter: "..i..' in '..self:getType(), true)
            end
        end
    end

    -- virtual
    Objective.requiredParams = {}

    function Objective:getText()
        env.error("Objective - getText not implemented")
        return "NOT IMPLEMENTED"
    end

    function Objective:update()
        env.error("Objective - update not implemented")
    end

    function Objective:checkFail()
        env.error("Objective - checkFail not implemented")
    end
    --end virtual
end

-----------------[[ END OF Objectives/Objective.lua ]]-----------------



-----------------[[ Objectives/ObjAirKillBonus.lua ]]-----------------

ObjAirKillBonus = Objective:new(Objective.types.air_kill_bonus)
do
    ObjAirKillBonus.requiredParams = {
        ['attr'] = true,
        ['bonus'] = true,
        ['count'] = true,
        ['linkedObjectives'] = true
    }

    function ObjAirKillBonus:getText()
        local msg = 'Destroy: '
        for _,v in ipairs(self.param.attr) do
            msg = msg..v..', '
        end
        msg = msg:sub(1,#msg-2)
        msg = msg..'\n Kills increase mission reward (Ends when other objectives are completed)'
        msg = msg..'\n   Kills: '..self.param.count
        return msg
    end

    function ObjAirKillBonus:update()
        if not self.isComplete and not self.isFailed then
            local allcomplete = true
            for _,obj in pairs(self.param.linkedObjectives) do
                if obj.isFailed then self.isFailed = true end
                if not obj.isComplete then allcomplete = false end
            end

            self.isComplete = allcomplete
        end
    end

    function ObjAirKillBonus:checkFail()
        if not self.isComplete and not self.isFailed then
            local allcomplete = true
            for _,obj in pairs(self.param.linkedObjectives) do
                if obj.isFailed then self.isFailed = true end
                if not obj.isComplete then allcomplete = false end
            end

            self.isComplete = allcomplete
        end
    end
end

-----------------[[ END OF Objectives/ObjAirKillBonus.lua ]]-----------------



-----------------[[ Objectives/ObjBombInsideZone.lua ]]-----------------

ObjBombInsideZone = Objective:new(Objective.types.bomb_in_zone)
do
    ObjBombInsideZone.requiredParams = {
        ['targetZone'] = true,
        ['max'] = true,
        ['required'] = true,
        ['dropped'] = true,
        ['isFinishStarted'] = true,
        ['bonus'] = true
    }

    function ObjBombInsideZone:getText()
        local msg = 'Bomb runways at '..self.param.targetZone.name..'\n'

        local ratio = self.param.dropped/self.param.required
        local percent = string.format('%.1f',ratio*100)

        msg = msg..'\n  Runway bombed: '..percent..'%\n'

        msg = msg..'\n  Cluster bombs do not deal enough damage to complete this mission'

        return msg
    end

    function ObjBombInsideZone:update()
        if not self.isComplete and not self.isFailed then
            if self.param.targetZone.side ~= 1 then
                self.isFailed = true
                self.mission.failureReason = self.param.targetZone.name..' is no longer controlled by the enemy.'
            end

            if not self.param.isFinishStarted then
                if self.param.dropped >= self.param.required then
                    self.param.isFinishStarted = true
                    timer.scheduleFunction(function(o)
                        o.isComplete = true
                    end, self, timer.getTime()+5)
                end
            end
        end
    end

    function ObjBombInsideZone:checkFail()
        if not self.isComplete and not self.isFailed then
            if self.param.targetZone.side ~= 1 then
                self.isFailed = true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjBombInsideZone.lua ]]-----------------



-----------------[[ Objectives/ObjClearZoneOfUnitsWithAttribute.lua ]]-----------------

ObjClearZoneOfUnitsWithAttribute = Objective:new(Objective.types.clear_attr_at_zone)
do
    ObjClearZoneOfUnitsWithAttribute.requiredParams = {
        ['attr'] = true,
        ['tgtzone'] = true
    }

    function ObjClearZoneOfUnitsWithAttribute:getText()
        local msg = 'Clear '..self.param.tgtzone.name..' of: '
        for _,v in ipairs(self.param.attr) do
            msg = msg..v..', '
        end
        msg = msg:sub(1,#msg-2)
        msg = msg..'\n Progress: '..self.param.tgtzone:getUnitCountWithAttributeOnSide(self.param.attr, 1)..' left'
        return msg
    end

    function ObjClearZoneOfUnitsWithAttribute:update()
        if not self.isComplete and not self.isFailed then
            local zn = self.param.tgtzone
            if zn.side ~= 1 or not zn:hasUnitWithAttributeOnSide(self.param.attr, 1) then 
                self.isComplete = true
                return true
            end
        end
    end

    function ObjClearZoneOfUnitsWithAttribute:checkFail()
        -- can not fail
    end
end

-----------------[[ END OF Objectives/ObjClearZoneOfUnitsWithAttribute.lua ]]-----------------



-----------------[[ Objectives/ObjDestroyGroup.lua ]]-----------------

ObjDestroyGroup = Objective:new(Objective.types.destroy_group)
do
    ObjDestroyGroup.requiredParams = {
        ['target'] = true,
        ['targetUnitNames'] = true,
        ['lastUpdate'] = true
    }

    function ObjDestroyGroup:getText()
        local msg = 'Destroy '..self.param.target.product.display..' before it reaches its destination.\n'

        local gr = Group.getByName(self.param.target.name)
        if gr and gr:getSize()>0 then
            local killcount = 0
            for i,v in pairs(self.param.targetUnitNames) do
                if v == true then
                    killcount = killcount + 1
                end
            end

            msg = msg..'\n     '..gr:getSize()..' units remaining. (killed '..killcount..')\n'
            for name, unit in pairs(self.mission.players) do
                if unit and unit:isExist() then
                    local tgtUnit = gr:getUnit(1)
                    local dist = mist.utils.get2DDist(unit:getPoint(), tgtUnit:getPoint())
                    
                    local m = '\n     '..name..': Distance: '
                    m = m..string.format('%.2f',dist/1000)..'km'
                    m = m..' Bearing: '..math.floor(Utils.getBearing(unit:getPoint(), tgtUnit:getPoint()))
                    msg = msg..m
                end
            end
        end

        return msg
    end

    function ObjDestroyGroup:update()
        if not self.isComplete and not self.isFailed then
            local target = self.param.target
            local exists = false
            local gr = Group.getByName(target.name)

            if gr and gr:getSize() > 0 then
                local updateFrequency = 5 -- seconds
                local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency

                if shouldUpdateMsg then
                    for _, unit in pairs(self.mission.players) do
                        if unit and unit:isExist() then
                            local tgtUnit = gr:getUnit(1)
                            local dist = mist.utils.get2DDist(unit:getPoint(), tgtUnit:getPoint())
                            local dstkm = string.format('%.2f',dist/1000)
                            local dstnm = string.format('%.2f',dist/1852)

                            local m = 'Distance: '
                            m = m..dstkm..'km | '..dstnm..'nm'

                            m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), tgtUnit:getPoint()))
                            trigger.action.outTextForUnit(unit:getID(), m, updateFrequency)
                        end
                    end
                    
                    self.param.lastUpdate = timer.getAbsTime()
                end
            elseif target.state == 'enroute' then
                for i,v in pairs(self.param.targetUnitNames) do
                    if v == true then
                        self.isComplete = true
                        return true
                    end
                end

                self.isFailed = true
                self.mission.failureReason = 'Convoy was killed by someone else.'
                return true
            else
                self.isFailed = true
                self.mission.failureReason = 'Convoy has reached its destination.'
                return true
            end
        end
    end

    function ObjDestroyGroup:checkFail()
        if not self.isComplete and not self.isFailed then
            local target = self.param.target
            local gr = Group.getByName(target.name)

            if target.state ~= 'enroute' or not gr or gr:getSize() == 0 then
                self.isFailed = true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjDestroyGroup.lua ]]-----------------



-----------------[[ Objectives/ObjDestroyStructure.lua ]]-----------------

ObjDestroyStructure = Objective:new(Objective.types.destroy_structure)
do
    ObjDestroyStructure.requiredParams = {
        ['target']=true,
        ['tgtzone']=true,
        ['killed']=true
    }

    function ObjDestroyStructure:getText()
        local msg = 'Destroy '..self.param.target.display..' at '..self.param.tgtzone.name..'\n'
            
        local point = nil
        local st = StaticObject.getByName(self.param.target.name)
        if st then
            point = st:getPoint()
        else
            st = Group.getByName(self.param.target.name)
            if st and st:getSize()>0 then
                point = st:getUnit(1):getPoint()
            end
        end
        
        if point then
            local lat,lon,alt = coord.LOtoLL(point)
            local mgrs = coord.LLtoMGRS(coord.LOtoLL(point))
            msg = msg..'\n DDM:  '.. mist.tostringLL(lat,lon,3)
            msg = msg..'\n DMS:  '.. mist.tostringLL(lat,lon,2,true)
            msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5)
            msg = msg..'\n Altitude: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft'
        end

        return msg
    end

    function ObjDestroyStructure:update()
        if not self.isComplete and not self.isFailed then
            if self.param.killed then
                self.isComplete = true
                return true
            end

            local target = self.param.target
            local exists = false
            local st = StaticObject.getByName(target.name)
            if st then
                exists = true
            else
                st = Group.getByName(target.name)
                if st and st:getSize()>0 then
                    exists = true
                end
            end

            if not exists then
                if not self.firstFailure then
                    self.firstFailure = timer.getAbsTime()
                end
            end

            if self.firstFailure and (timer.getAbsTime() - self.firstFailure > 1*60) then
                self.isFailed = true
                self.mission.failureReason = 'Structure was destoyed by someone else.'
                return true
            end
        end
    end

    function ObjDestroyStructure:checkFail()
        if not self.isComplete and not self.isFailed then
            local target = self.param.target
            local exists = false
            local st = StaticObject.getByName(target.name)
            if st then
                exists = true
            else
                st = Group.getByName(target.name)
                if st and st:getSize()>0 then
                    exists = true
                end
            end

            if not exists then 
                self.isFailed = true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjDestroyStructure.lua ]]-----------------



-----------------[[ Objectives/ObjDestroyUnitsWithAttribute.lua ]]-----------------

ObjDestroyUnitsWithAttribute = Objective:new(Objective.types.destroy_attr)
do
    ObjDestroyUnitsWithAttribute.requiredParams = {
        ['attr'] = true,
        ['amount'] = true,
        ['killed'] = true
    }

    function ObjDestroyUnitsWithAttribute:getText()
        local msg = 'Destroy: '
        for _,v in ipairs(self.param.attr) do
            msg = msg..v..', '
        end
        msg = msg:sub(1,#msg-2)
        msg = msg..'\n Progress: '..self.param.killed..'/'..self.param.amount
        return msg
    end

    function ObjDestroyUnitsWithAttribute:update()
        if not self.isComplete and not self.isFailed then
            if self.param.killed >= self.param.amount then
                self.isComplete = true
                return true
            end
        end
    end

    function ObjDestroyUnitsWithAttribute:checkFail()
        -- can not fail
    end
end

-----------------[[ END OF Objectives/ObjDestroyUnitsWithAttribute.lua ]]-----------------



-----------------[[ Objectives/ObjDestroyUnitsWithAttributeAtZone.lua ]]-----------------

ObjDestroyUnitsWithAttributeAtZone = Objective:new(Objective.types.destroy_attr_at_zone)
do
    ObjDestroyUnitsWithAttributeAtZone.requiredParams = {
        ['attr']=true,
        ['amount'] = true,
        ['killed'] = true,
        ['tgtzone'] = true
    }

    function ObjDestroyUnitsWithAttributeAtZone:getText()
        local msg = 'Destroy at '..self.param.tgtzone.name..': '
        for _,v in ipairs(self.param.attr) do
            msg = msg..v..', '
        end
        msg = msg:sub(1,#msg-2)
        msg = msg..'\n Progress: '..self.param.killed..'/'..self.param.amount
        return msg
    end

    function ObjDestroyUnitsWithAttributeAtZone:update()
        if not self.isComplete and not self.isFailed then
            if self.param.killed >= self.param.amount then
                self.isComplete = true
                return true
            end

            local zn = self.param.tgtzone
            if zn.side ~= 1 or not zn:hasUnitWithAttributeOnSide(self.param.attr, 1) then 
                if self.firstFailure == nil then
                    self.firstFailure = timer.getAbsTime()
                else
                    if timer.getAbsTime() - self.firstFailure > 5*60 then
                        self.isFailed = true
                        self.mission.failureReason = zn.name..' no longer has targets matching the description.'
                        return true
                    end
                end
            else
                if self.firstFailure ~= nil then
                    self.firstFailure = nil
                end
            end
        end
    end

    function ObjDestroyUnitsWithAttributeAtZone:checkFail()
        if not self.isComplete and not self.isFailed then
            local zn = self.param.tgtzone
            if zn.side ~= 1 or not zn:hasUnitWithAttributeOnSide(self.param.attr, 1) then 
                self.isFailed = true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjDestroyUnitsWithAttributeAtZone.lua ]]-----------------



-----------------[[ Objectives/ObjEscortGroup.lua ]]-----------------

ObjEscortGroup = Objective:new(Objective.types.escort)
do
    ObjEscortGroup.requiredParams = {
        ['maxAmount']=true,
        ['amount'] = true,
        ['proxDist']= true,
        ['target'] = true,
        ['lastUpdate']= true
    }

    function ObjEscortGroup:getText()
        local msg = 'Stay in close proximity of the convoy'

        local gr = Group.getByName(self.param.target.name)
        if gr and gr:getSize()>0 then
            local grunit = gr:getUnit(1)
            local lat,lon,alt = coord.LOtoLL(grunit:getPoint())
            local mgrs = coord.LLtoMGRS(coord.LOtoLL(grunit:getPoint()))
            msg = msg..'\n DDM:  '.. mist.tostringLL(lat,lon,3)
            msg = msg..'\n DMS:  '.. mist.tostringLL(lat,lon,2,true)
            msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5)
        end
        
        local prg = math.floor(((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100)
        msg = msg.. '\n Progress: '..prg..'%'
        return msg
    end

    function ObjEscortGroup:update()
        if not self.isComplete and not self.isFailed then
            local gr = Group.getByName(self.param.target.name)
            if not gr or gr:getSize()==0 then
                self.isFailed = true
                self.mission.failureReason = 'Group has been destroyed.'
                return true
            end
            local grunit = gr:getUnit(1)

            if self.param.target.state == 'atdestination' or self.param.target.state == 'siege' then
                for name, unit in pairs(self.mission.players) do
                    if unit and unit:isExist() then
                        local dist = mist.utils.get3DDist(unit:getPoint(), grunit:getPoint())
                        if dist < self.param.proxDist then
                            self.isComplete = true
                            break
                        end
                    end
                end

                if not self.isComplete then 
                    self.isFailed = true 
                    self.mission.failureReason = 'Group has reached its destination without an escort.'
                end
            end

            if not self.isComplete and not self.isFailed then
                local plycount = Utils.getTableSize(self.mission.players)
                if plycount == 0 then plycount = 1 end
                local updateFrequency = 5 -- seconds
                local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency
                for name, unit in pairs(self.mission.players) do
                    if unit and unit:isExist() then
                        local dist = mist.utils.get3DDist(unit:getPoint(), grunit:getPoint())
                        if dist < self.param.proxDist then
                            self.param.amount = self.param.amount - (1/plycount)

                            if shouldUpdateMsg then
                                local prg = string.format('%.1f',((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100)
                                trigger.action.outTextForUnit(unit:getID(), 'Progress: '..prg..'%', updateFrequency)
                            end
                        else
                            if shouldUpdateMsg then
                                local m = 'Distance: '
                                if dist>1000 then
                                    local dstkm = string.format('%.2f',dist/1000)
                                    local dstnm = string.format('%.2f',dist/1852)

                                    m = m..dstkm..'km | '..dstnm..'nm'
                                else
                                    local dstft = math.floor(dist/0.3048)
                                    m = m..math.floor(dist)..'m | '..dstft..'ft'
                                end

                                m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), grunit:getPoint()))
                                trigger.action.outTextForUnit(unit:getID(), m, updateFrequency)
                            end
                        end
                    end
                end

                if shouldUpdateMsg then
                    self.param.lastUpdate = timer.getAbsTime()
                end
            end

            if self.param.amount <= 0 then
                self.isComplete = true
                return true
            end
        end
    end

    function ObjEscortGroup:checkFail()
        if not self.isComplete and not self.isFailed then
            local tg = self.param.target
            local gr = Group.getByName(tg.name)
            if not gr or gr:getSize() == 0 then
                self.isFailed = true
            end

            if self.mission.state == Mission.states.new then
                if tg.state == 'enroute' and (timer.getAbsTime() - tg.lastStateTime) >= 7*60 then
                    self.isFailed = true
                end
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjEscortGroup.lua ]]-----------------



-----------------[[ Objectives/ObjFlyToZoneSequence.lua ]]-----------------

ObjFlyToZoneSequence = Objective:new(Objective.types.fly_to_zone_seq)
do
    ObjFlyToZoneSequence.requiredParams = {
        ['waypoints'] = true,
        ['failZones'] = true
    }

    function ObjFlyToZoneSequence:getText()
        local msg = 'Fly route: '
            
        for i,v in ipairs(self.param.waypoints) do
            if v.complete then
                msg = msg..'\n [✓] '..i..'. '..v.zone.name
            else
                msg = msg..'\n --> '..i..'. '..v.zone.name
            end
        end
        return msg
    end

    function ObjFlyToZoneSequence:update()
        if not self.isComplete and not self.isFailed then
            if self.param.failZones[1] then
                for _,zn in ipairs(self.param.failZones[1]) do
                    if zn.side ~= 1 then 
                        self.isFailed = true
                        self.mission.failureReason = zn.name..' is no longer controlled by the enemy.'
                        break
                    end
                end
            end

            if self.param.failZones[2] then
                for _,zn in ipairs(self.param.failZones[2]) do
                    if zn.side ~= 2 then 
                        self.isFailed = true
                        self.mission.failureReason = zn.name..' was lost.'
                        break
                    end
                end
            end

            if not self.isFailed then
                local firstWP = nil
                local nextWP = nil
                for i,leg in ipairs(self.param.waypoints) do
                    if not leg.complete then
                        firstWP = leg
                        nextWP = self.param.waypoints[i+1]
                        break
                    end
                end

                if firstWP then
                    local point = firstWP.zone.zone.point
                    local range = 3000 --meters
                    local allInside = true
                    for p,u in pairs(self.mission.players) do
                        if u and u:isExist() then
                            if Utils.isLanded(u,true) then
                                allInside = false
                                break
                            end
                            
                            local pos = u:getPoint()
                            local dist = mist.utils.get2DDist(point, pos)
                            if dist > range then
                                allInside = false
                                break
                            end
                        end
                    end

                    if allInside then
                        firstWP.complete = true
                        self.mission:pushMessageToPlayers(firstWP.zone.name..' reached')
                        if nextWP then
                            self.mission:pushMessageToPlayers('Next point: '..nextWP.zone.name)
                        end
                    end
                else
                    self.isComplete = true
                    return true
                end
            end
        end
    end

    function ObjFlyToZoneSequence:checkFail()
        if not self.isComplete and not self.isFailed then
            if self.param.failZones[1] then
                for _,zn in ipairs(self.param.failZones[1]) do
                    if zn.side ~= 1 then 
                        self.isFailed = true
                        break
                    end
                end
            end

            if self.param.failZones[2] then
                for _,zn in ipairs(self.param.failZones[2]) do
                    if zn.side ~= 2 then 
                        self.isFailed = true
                        break
                    end
                end
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjFlyToZoneSequence.lua ]]-----------------



-----------------[[ Objectives/ObjProtectMission.lua ]]-----------------

ObjProtectMission = Objective:new(Objective.types.protect)
do
    ObjProtectMission.requiredParams = {
        ['mis'] = true
    }

    function ObjProtectMission:getText()
        local msg = 'Prevent enemy aircraft from interfering with '..self.param.mis:getMissionName()..' mission.'

        if self.param.mis.info and self.param.mis.info.targetzone then
            msg = msg..'\n Target zone: '..self.param.mis.info.targetzone.name
        end

        msg = msg..'\n Protect players: '
        for i,v in pairs(self.param.mis.players) do
            msg = msg..'\n  '..i
        end
        
        msg = msg..'\n Mission success depends on '..self.param.mis:getMissionName()..' mission success.'
        return msg
    end

    function ObjProtectMission:update()
        if not self.isComplete and not self.isFailed then
            if self.param.mis.state == Mission.states.failed then 
                self.isFailed = true
                self.mission.failureReason = "Failed to protect players of "..self.param.mis.name.." mission."
            end

            if self.param.mis.state == Mission.states.completed then 
                self.isComplete = true
            end
        end
    end

    function ObjProtectMission:checkFail()
        if not self.isComplete and not self.isFailed then
            if self.param.mis.state == Mission.states.failed then 
                self.isFailed = true
            end

            if self.param.mis.state == Mission.states.completed then 
                if self.mission.state == Mission.states.new or 
                    self.mission.state == Mission.states.preping or 
                    self.mission.state == Mission.states.comencing then
                        
                    self.isFailed = true
                end
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjProtectMission.lua ]]-----------------



-----------------[[ Objectives/ObjReconZone.lua ]]-----------------

ObjReconZone = Objective:new(Objective.types.recon_zone)
do
    ObjReconZone.requiredParams = { 
        ['target'] = true,
        ['failZones'] = true
    }

    function ObjReconZone:getText()
        local msg = 'Conduct a recon mission on '..self.param.target.name..' and return with data to a friendly zone.'

        return msg
    end

    function ObjReconZone:update()
        if not self.isComplete and not self.isFailed then
            if self.param.failZones[1] then
                for _,zn in ipairs(self.param.failZones[1]) do
                    if zn.side ~= 1 then 
                        self.isFailed = true
                        self.mission.failureReason = zn.name..' is no longer controlled by the enemy.'
                        break
                    end
                end
            end

            if self.param.failZones[2] then
                for _,zn in ipairs(self.param.failZones[2]) do
                    if zn.side ~= 2 then 
                        self.isFailed = true
                        break
                    end
                end
            end

            if not self.isFailed then
                if self.param.reconData then
                    self.isComplete = true
                    return true
                end
            end
        end
    end

    function ObjReconZone:checkFail()
        if not self.isComplete and not self.isFailed then
            if self.param.failZones[1] then
                for _,zn in ipairs(self.param.failZones[1]) do
                    if zn.side ~= 1 then 
                        self.isFailed = true
                        break
                    end
                end
            end

            if self.param.failZones[2] then
                for _,zn in ipairs(self.param.failZones[2]) do
                    if zn.side ~= 2 then 
                        self.isFailed = true
                        break
                    end
                end
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjReconZone.lua ]]-----------------



-----------------[[ Objectives/ObjSupplyZone.lua ]]-----------------

ObjSupplyZone = Objective:new(Objective.types.supply)
do
    ObjSupplyZone.requiredParams = {
        ['amount']=true,
        ['delivered']=true,
        ['tgtzone']=true
    }

    function ObjSupplyZone:getText()
        local msg = 'Deliver '..self.param.amount..' to '..self.param.tgtzone.name..': '
        msg = msg..'\n Progress: '..self.param.delivered..'/'..self.param.amount
        return msg
    end

    function ObjSupplyZone:update()
        if not self.isComplete and not self.isFailed then
            if self.param.delivered >= self.param.amount then
                self.isComplete = true
                return true
            end

            local zn = self.param.tgtzone
            if zn.side ~= 2 then 
                self.isFailed = true
                self.mission.failureReason = zn.name..' was lost.'
                return true
            end
        end
    end

    function ObjSupplyZone:checkFail()
        if not self.isComplete and not self.isFailed then
            local zn = self.param.tgtzone
            if zn.side ~= 2 then 
                self.isFailed = true
                return true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjSupplyZone.lua ]]-----------------



-----------------[[ Objectives/ObjExtractSquad.lua ]]-----------------

ObjExtractSquad = Objective:new(Objective.types.extract_squad)
do
    ObjExtractSquad.requiredParams = {
        ['target']=true,
        ['loadedBy']=false,
        ['lastUpdate']= true
    }

    function ObjExtractSquad:getText()
        local infName = PlayerLogistics.getInfantryName(self.param.target.data.type)
        local msg = 'Extract '..infName..' '..self.param.target.name..'\n'
        
        if not self.param.loadedBy then
            local gr = Group.getByName(self.param.target.name)
            if gr and gr:getSize()>0 then
                local point = gr:getUnit(1):getPoint()

                local lat,lon,alt = coord.LOtoLL(point)
                local mgrs = coord.LLtoMGRS(coord.LOtoLL(point))
                msg = msg..'\n DDM:  '.. mist.tostringLL(lat,lon,3)
                msg = msg..'\n DMS:  '.. mist.tostringLL(lat,lon,2,true)
                msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5)
                msg = msg..'\n Altitude: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft'
            end
        end

        return msg
    end

    function ObjExtractSquad:update()
        if not self.isComplete and not self.isFailed then

            if self.param.loadedBy then
                self.isComplete = true
                return true
            else
                local target = self.param.target
                
                local gr = Group.getByName(target.name)
                if not gr or gr:getSize()==0 then
                    self.isFailed = true
                    self.mission.failureReason = 'Squad was not rescued in time, and went MIA.'
                    return true
                end
            end

            local updateFrequency = 5 -- seconds
            local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency
            if shouldUpdateMsg then
                for name, unit in pairs(self.mission.players) do
                    if unit and unit:isExist() then
                        local gr = Group.getByName(self.param.target.name)
                        local un = gr:getUnit(1)
                        local dist = mist.utils.get3DDist(unit:getPoint(), un:getPoint())
                        local m = 'Distance: '
                        if dist>1000 then
                            local dstkm = string.format('%.2f',dist/1000)
                            local dstnm = string.format('%.2f',dist/1852)

                            m = m..dstkm..'km | '..dstnm..'nm'
                        else
                            local dstft = math.floor(dist/0.3048)
                            m = m..math.floor(dist)..'m | '..dstft..'ft'
                        end

                        m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), un:getPoint()))           
                        trigger.action.outTextForUnit(unit:getID(), m, updateFrequency)
                    end
                end

                self.param.lastUpdate = timer.getAbsTime()
            end
        end
    end

    function ObjExtractSquad:checkFail()
        if not self.isComplete and not self.isFailed then
            local target = self.param.target
                
            local gr = Group.getByName(target.name)
            if not gr or not gr:isExist() or gr:getSize()==0 then
                self.isFailed = true
                return true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjExtractSquad.lua ]]-----------------



-----------------[[ Objectives/ObjExtractPilot.lua ]]-----------------

ObjExtractPilot = Objective:new(Objective.types.extract_pilot)
do
    ObjExtractPilot.requiredParams = {
        ['target']=true,
        ['loadedBy']=false,
        ['lastUpdate']= true
    }

    function ObjExtractPilot:getText()
        local msg = 'Rescue '..self.param.target.name..'\n'
        
        if not self.param.loadedBy then
            
            if self.param.target.pilot:isExist() and 
                self.param.target.pilot:getSize() > 0 and 
                self.param.target.pilot:getUnit(1):isExist() then
                    
                local point = self.param.target.pilot:getUnit(1):getPoint()

                local lat,lon,alt = coord.LOtoLL(point)
                local mgrs = coord.LLtoMGRS(coord.LOtoLL(point))
                msg = msg..'\n DDM:  '.. mist.tostringLL(lat,lon,3)
                msg = msg..'\n DMS:  '.. mist.tostringLL(lat,lon,2,true)
                msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5)
                msg = msg..'\n Altitude: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft'
            end
        end

        return msg
    end

    function ObjExtractPilot:update()
        if not self.isComplete and not self.isFailed then

            if self.param.loadedBy then
                self.isComplete = true
                return true
            else
                if not self.param.target.pilot:isExist() or self.param.target.remainingTime <= 0 then
                    self.isFailed = true
                    self.mission.failureReason = 'Pilot was not rescued in time, and went MIA.'
                    return true
                end
            end

            local updateFrequency = 5 -- seconds
            local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency
            if shouldUpdateMsg then
                for name, unit in pairs(self.mission.players) do
                    if unit and unit:isExist() then
                        local gr = Group.getByName(self.param.target.name)
                        if gr and gr:getSize() > 0 then
                            local un = gr:getUnit(1)
                            if un then
                                local dist = mist.utils.get3DDist(unit:getPoint(), un:getPoint())
                                local m = 'Distance: '
                                if dist>1000 then
                                    local dstkm = string.format('%.2f',dist/1000)
                                    local dstnm = string.format('%.2f',dist/1852)

                                    m = m..dstkm..'km | '..dstnm..'nm'
                                else
                                    local dstft = math.floor(dist/0.3048)
                                    m = m..math.floor(dist)..'m | '..dstft..'ft'
                                end

                                m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), un:getPoint()))
                                trigger.action.outTextForUnit(unit:getID(), m, updateFrequency)
                            end
                        end
                    end
                end

                self.param.lastUpdate = timer.getAbsTime()
            end
        end
    end

    function ObjExtractPilot:checkFail()
        if not self.isComplete and not self.isFailed then
            if not self.param.target.pilot:isExist() or self.param.target.remainingTime <= 0 then
                self.isFailed = true
                return true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjExtractPilot.lua ]]-----------------



-----------------[[ Objectives/ObjUnloadExtractedPilotOrSquad.lua ]]-----------------

ObjUnloadExtractedPilotOrSquad = Objective:new(Objective.types.unloaded_pilot_or_squad)
do
    ObjUnloadExtractedPilotOrSquad.requiredParams = {
        ['targetZone']=false,
        ['extractObjective']=true,
        ['unloadedAt']=false
    }

    function ObjUnloadExtractedPilotOrSquad:getText()
        local msg = 'Drop off personnel '
        if self.param.targetZone then
            msg = msg..'at '..self.param.targetZone.name..'\n'
        else
            msg = msg..'at a friendly zone\n'
        end

        return msg
    end

    function ObjUnloadExtractedPilotOrSquad:update()
        if not self.isComplete and not self.isFailed then

            if self.param.extractObjective.isComplete and self.param.unloadedAt then
                if self.param.targetZone then
                    if self.param.unloadedAt == self.param.targetZone.name then
                        self.isComplete = true
                        return true
                    else
                        self.isFailed = true
                        self.mission.failureReason = 'Personnel dropped off at wrong zone.'
                        return true
                    end
                else
                    self.isComplete = true
                    return true
                end
            end

            if self.param.extractObjective.isFailed then
                self.isFailed = true
                return true
            end

            if self.param.targetZone and self.param.targetZone.side ~= 2 then
                self.isFailed = true
                self.mission.failureReason = self.param.targetZone.name..' was lost.'
                return true
            end
        end
    end

    function ObjUnloadExtractedPilotOrSquad:checkFail()
        if not self.isComplete and not self.isFailed then

            if self.param.extractObjective.isFailed then
                self.isFailed = true
                return true
            end

            if self.param.targetZone and self.param.targetZone.side ~= 2 then
                self.isFailed = true
                return true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjUnloadExtractedPilotOrSquad.lua ]]-----------------



-----------------[[ Objectives/ObjPlayerCloseToZone.lua ]]-----------------

ObjPlayerCloseToZone = Objective:new(Objective.types.player_close_to_zone)
do
    ObjPlayerCloseToZone.requiredParams = {
        ['target']=true,
        ['range'] = true,
        ['amount']= true,
        ['maxAmount'] = true,
        ['lastUpdate']= true
    }

    function ObjPlayerCloseToZone:getText()
        local msg = 'Patrol area around '..self.param.target.name

        local prg = math.floor(((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100)
        msg = msg.. '\n Progress: '..prg..'%'
        return msg
    end

    function ObjPlayerCloseToZone:update()
        if not self.isComplete and not self.isFailed then

            if self.param.target.side ~= 2 then
                self.isFailed = true
                self.mission.failureReason = self.param.target.name..' was lost.'
                return true
            end

            local plycount = Utils.getTableSize(self.mission.players)
            if plycount == 0 then plycount = 1 end
            local updateFrequency = 5 -- seconds
            local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency
            for name, unit in pairs(self.mission.players) do
                if unit and unit:isExist() and Utils.isInAir(unit) then
                    local dist = mist.utils.get2DDist(unit:getPoint(), self.param.target.zone.point)
                    if dist < self.param.range then
                        self.param.amount = self.param.amount - (1/plycount)

                        if shouldUpdateMsg then
                            local prg = string.format('%.1f',((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100)
                            trigger.action.outTextForUnit(unit:getID(), '['..self.param.target.name..'] Progress: '..prg..'%', updateFrequency)
                        end
                    end
                end
            end

            if shouldUpdateMsg then
                self.param.lastUpdate = timer.getAbsTime()
            end

            if self.param.amount <= 0 then
                self.isComplete = true
                for name, unit in pairs(self.mission.players) do
                    if unit and unit:isExist() and Utils.isInAir(unit) then
                        trigger.action.outTextForUnit(unit:getID(), '['..self.param.target.name..'] Complete', updateFrequency)
                    end
                end
                return true
            end
        end
    end

    function ObjPlayerCloseToZone:checkFail()
        if not self.isComplete and not self.isFailed then
            if self.param.target.side ~= 2 then
                self.isFailed = true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjPlayerCloseToZone.lua ]]-----------------



-----------------[[ Objectives/ObjDeploySquad.lua ]]-----------------

ObjDeploySquad = Objective:new(Objective.types.deploy_squad)
do
    ObjDeploySquad.requiredParams = {
        ['squadType']=true,
        ['targetZone']=true,
        ['requiredZoneSide']=true,
        ['unloadedType']=false,
        ['unloadedAt']=false
    }

    function ObjDeploySquad:getText()
        local infName = PlayerLogistics.getInfantryName(self.param.squadType)
        local msg = 'Deploy '..infName..' at '..self.param.targetZone.name
        return msg
    end

    function ObjDeploySquad:update()
        if not self.isComplete and not self.isFailed then

            if self.param.unloadedType and self.param.unloadedAt then
                if self.param.targetZone.name == self.param.unloadedAt then
                    if self.param.squadType == self.param.unloadedType then
                        self.isComplete = true
                        return true
                    end
                end
            end

            if self.param.targetZone.side ~= self.param.requiredZoneSide then
                self.isFailed = true

                local side = ''
                if self.param.requiredZoneSide == 0 then side = 'neutral' 
                elseif self.param.requiredZoneSide == 1 then side = 'controlled by Red'
                elseif self.param.requiredZoneSide == 2 then side = 'controlled by Blue'
                end

                self.mission.failureReason = self.param.targetZone.name..' is no longer '..side
                return true
            end
        end
    end

    function ObjDeploySquad:checkFail()
        if not self.isComplete and not self.isFailed then
            if self.param.targetZone.side ~= self.param.requiredZoneSide then
                self.isFailed = true
                return true
            end
        end
    end
end

-----------------[[ END OF Objectives/ObjDeploySquad.lua ]]-----------------



-----------------[[ Missions/Mission.lua ]]-----------------

Mission = {}
do
    Mission.states = {
        new = 'new',                -- mission was just generated and is listed publicly
        preping = 'preping',        -- mission was accepted by a player, was delisted, and player recieved a join code that can be shared
        comencing = 'comencing',    -- a player that is subscribed to the mission has taken off, join code is invalidated
        active = 'active',          -- all players subscribed to the mission have taken off, objective can now be accomplished
        completed = 'completed',    -- mission objective was completed, players need to land to claim rewards
        failed = 'failed'           -- mission lost all players OR mission objective no longer possible to accomplish
    }

    --[[
        new -> preping -> comencing -> active -> completed
         |      |         |           |-> failed
         |      |         |->failed
         |      |->failed
         |->failed
    --]]

    Mission.types = {
        cap_easy = 'cap_easy',          -- fly over zn A-B-A-B-A-B OR destroy few enemy aircraft 
        cap_medium = 'cap_medium',      -- fly over zn A-B-A-B-A-B AND destroy few enemy aircraft -- push list of aircraft within range of target zones
        tarcap = 'tarcap',              -- protect other mission, air kills increase reward
        --tarcap = 'tarcap',            -- update target mission list after all other missions are in

        cas_easy = 'cas_easy',          -- destroy small amount of ground units
        cas_medium = 'cas_medium',      -- destroy large amount of ground units
        cas_hard = 'cas_hard',          -- destroy all defenses at zone A
        bai = 'bai',                    -- destroy any enemy convoy - show "last" location of convoi (BRA or LatLon) update every 30 seconds
        
        sead = 'sead',                  -- destroy any SAM TR or SAM SR at zone A
        dead = 'dead',                  -- destroy all SAM TR or SAM SR, or IR Guided SAM at zone A
        
        strike_veryeasy = 'strike_veryeasy',   -- destroy 1 building
        strike_easy = 'strike_easy',    -- destroy any structure at zone A
        strike_medium = 'strike_medium',-- destroy specific structure at zone A - show LatLon and Alt in mission description
        strike_hard = 'strike_hard',    -- destroy all structures at zone A and turn it neutral
        deep_strike = 'deep_strike',   -- destroy specific structure taken from strike queue - show LatLon and Alt in mission description

        anti_runway = 'anti_runway',  -- drop at least X anti runway bombs on runway zone (if player unit launches correct weapon, track, if agl>10m check if in zone, tally), define list of runway zones somewhere
        
        supply_easy = 'supply_easy',   -- transfer resources to zone A(low supply)
        supply_hard = 'supply_hard',   -- transfer resources to zone A(low supply), high resource number
        escort = 'escort',              -- follow and protect friendly convoy until they get to target OR 10 minutes pass
        csar = 'csar',                  -- extract specific pilot to friendly zone, track friendly pilots ejected
        recon = 'recon',                -- conduct recon
        extraction = 'extraction',  -- extract a deployed squad to friendly zone, generate mission if squad has extractionReady state
        deploy_squad = 'deploy_squad',  -- deploy squad to zone
    }

    Mission.completion_type = {
        any = 'any',
        all = 'all'
    }

    function Mission:new(id, type)
        local expire = math.random(60*15, 60*30)

		local obj = {
            missionID = id,
            type = type,
            name = '',
            description = '',
            failureReason = nil,
            state = Mission.states.new,
            expireTime = expire,
            lastStateTime = timer.getAbsTime(),
            objectives = {},
            completionType = Mission.completion_type.any,
            rewards = {},
            players = {},
            info = {}
        }

		setmetatable(obj, self)
		self.__index = self
		
        if obj.getExpireTime then obj.expireTime = obj:getExpireTime() end
        if obj.getMissionName then obj.name = obj:getMissionName() end
        if obj.generateObjectives then obj:generateObjectives() end
        if obj.generateRewards then obj:generateRewards() end

		return obj
	end

    function Mission:updateState(newstate)
        env.info('Mission - code'..self.missionID..' updateState state changed from '..self.state..' to '..newstate)
        self.state = newstate
        self.lastStateTime = timer.getAbsTime()
        if self.state == self.states.preping then
            if self.info.targetzone then
                MissionTargetRegistry.addZone(self.info.targetzone.name)
            end
        elseif self.state == self.states.completed or self.state == self.states.failed then
            if self.info.targetzone then
                MissionTargetRegistry.removeZone(self.info.targetzone.name)
            end
        end
    end

    function Mission:pushMessageToPlayer(player, msg, duration)
        if not duration then
            duration = 10
        end

        for name,un in pairs(self.players) do
            if name == player and un and un:isExist() then
                trigger.action.outTextForUnit(un:getID(), msg, duration)
                break
            end
        end
    end

    function Mission:pushMessageToPlayers(msg, duration)
        if not duration then
            duration = 10
        end

        for _,un in pairs(self.players) do
            if un and un:isExist() then
                trigger.action.outTextForUnit(un:getID(), msg, duration)
            end
        end
    end

    function Mission:pushSoundToPlayers(sound)
        for _,un in pairs(self.players) do
            if un and un:isExist() then
                --trigger.action.outSoundForUnit(un:getID(), sound) -- does not work correctly in multiplayer
                trigger.action.outSoundForGroup(un:getGroup():getID(), sound)
            end
        end
    end

    function Mission:removePlayer(player)
        for pl,un in pairs(self.players) do
            if pl == player then
                self.players[pl] = nil
                break
            end
        end
    end

    function Mission:isInstantReward()
        return false
    end

    function Mission:hasPlayers()
        return Utils.getTableSize(self.players) > 0
    end

    function Mission:getPlayerUnit(player)
        return self.players[player]
    end

    function Mission:addPlayer(player, unit)
        self.players[player] = unit
    end

    function Mission:checkFailConditions()
        if self.state == Mission.states.active then return end

        for _,obj in ipairs(self.objectives) do
            local shouldBreak = obj:checkFail()
            
            if shouldBreak then break end
        end
    end

    function Mission:updateObjectives()
        if self.state ~= self.states.active then return end

        for _,obj in ipairs(self.objectives) do
            local shouldBreak = obj:update()

            if obj.isFailed and self.objectiveFailedCallback then self:objectiveFailedCallback(obj) end
            if not obj.isFailed and obj.isComplete and self.objectiveCompletedCallback then self:objectiveCompletedCallback(obj) end

            if shouldBreak then break end
        end
    end

    function Mission:updateIsFailed()
        self:checkFailConditions()

        local allFailed = true
        for _,obj in ipairs(self.objectives) do
            if self.state == Mission.states.new then
                if obj.isFailed then
                    self:updateState(Mission.states.failed)
                    env.info("Mission code"..self.missionID.." objective cancelled:\n"..obj:getText())
                    break
                end
            end

            if self.completionType == Mission.completion_type.all then
                if obj.isFailed then
                    self:updateState(Mission.states.failed)
                    env.info("Mission code"..self.missionID.." (all) objective failed:\n"..obj:getText())
                    break
                end
            end

            if not obj.isFailed then
                allFailed = false
            end
        end

        if self.completionType == Mission.completion_type.any and allFailed then
            self:updateState(Mission.states.failed)
            env.info("Mission code"..self.missionID.." all objectives failed")
        end
    end

    function Mission:updateIsCompleted()
        if self.completionType == self.completion_type.any then
            for _,obj in ipairs(self.objectives) do
                if obj.isComplete then 
                    self:updateState(self.states.completed)
                    env.info("Mission code"..self.missionID.." (any) objective completed:\n"..obj:getText())
                    break
                end
            end
        elseif self.completionType == self.completion_type.all then
            local allComplete = true
            for _,obj in ipairs(self.objectives) do
                if not obj.isComplete then
                    allComplete = false
                    break
                end
            end

            if allComplete then
                self:updateState(self.states.completed)
                env.info("Mission code"..self.missionID.." all objectives complete")
            end
        end
    end

    function Mission:tallyWeapon(weapon)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjBombInsideZone:getType() then
                    for i,v in ipairs(obj.param.targetZone:getRunwayZones()) do
                        if Utils.isInZone(weapon, v.name) then
                            if obj.param.dropped < obj.param.max then
                                obj.param.dropped = obj.param.dropped + 1
                                if obj.param.dropped > obj.param.required then
                                    for _,rew in ipairs(self.rewards) do
                                        if obj.param.bonus[rew.type] then
                                            rew.amount = rew.amount +  obj.param.bonus[rew.type]

                                            if rew.type == PlayerTracker.statTypes.xp then
                                                self:pushMessageToPlayers("Bonus: + "..obj.param.bonus[rew.type]..' XP')
                                            end
                                        end
                                    end
                                end
                            end
                            break
                        end
                    end
                end
            end
        end
    end

    function Mission:tallyKill(kill)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjDestroyUnitsWithAttribute:getType() then
                    for _,a in ipairs(obj.param.attr) do
                        if kill:hasAttribute(a) then
                            obj.param.killed = obj.param.killed + 1
                            break
                        elseif a == 'Buildings' and ZoneCommand and ZoneCommand.staticRegistry[kill:getName()] then
                            obj.param.killed = obj.param.killed + 1
                            break
                        end
                    end
                elseif obj.type == ObjDestroyStructure:getType() then
                    if obj.param.target.name == kill:getName() then
                        obj.param.killed = true
                    end
                elseif obj.type == ObjDestroyGroup:getType() then
                    if kill.getName then
                        if obj.param.targetUnitNames[kill:getName()] ~= nil then
                            obj.param.targetUnitNames[kill:getName()] = true
                        end
                    end
                elseif obj.type == ObjAirKillBonus:getType() then
                    for _,a in ipairs(obj.param.attr) do
                        if kill:hasAttribute(a) then
                            for _,rew in ipairs(self.rewards) do
                                if obj.param.bonus[rew.type] then
                                    rew.amount = rew.amount +  obj.param.bonus[rew.type]
                                    obj.param.count = obj.param.count + 1
                                    if rew.type == PlayerTracker.statTypes.xp then
                                        self:pushMessageToPlayers("Reward increased: + "..obj.param.bonus[rew.type]..' XP')
                                    end
                                end
                            end
                            break
                        elseif a == 'Buildings' and ZoneCommand and ZoneCommand.staticRegistry[kill:getName()] then
                            for _,rew in ipairs(self.rewards) do
                                if obj.param.bonus[rew.type] then
                                    rew.amount = rew.amount +  obj.param.bonus[rew.type]
                                    obj.param.count = obj.param.count + 1
                                    
                                    if rew.type == PlayerTracker.statTypes.xp then
                                        self:pushMessageToPlayers("Reward increased: + "..obj.param.bonus[rew.type]..' XP')
                                    end
                                end
                            end
                            break
                        end
                    end
                elseif obj.type == ObjDestroyUnitsWithAttributeAtZone:getType() then
                    local zn = obj.param.tgtzone
                    if zn then
                        local validzone = false
                        if Utils.isInZone(kill, zn.name) then
                            validzone = true
                        else
                            for nm,_ in pairs(zn.built) do
                                local gr = Group.getByName(nm)
                                if gr then
                                    for _,un in ipairs(gr:getUnits()) do
                                        if un:getID() == kill:getID() then
                                            validzone = true
                                            break
                                        end
                                    end
                                end

                                if validzone then break end
                            end
                        end
                        
                        if validzone then
                            for _,a in ipairs(obj.param.attr) do
                                if kill:hasAttribute(a) then
                                    obj.param.killed = obj.param.killed + 1
                                    break
                                elseif a == 'Buildings' and ZoneCommand and ZoneCommand.staticRegistry[kill:getName()] then
                                    obj.param.killed = obj.param.killed + 1
                                    break
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    function Mission:isUnitTypeAllowed(unit)
        return true
    end

    function Mission:tallySupplies(amount, zonename)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjSupplyZone:getType() then
                    if obj.param.tgtzone.name == zonename then
                        obj.param.delivered = obj.param.delivered + amount
                    end
                end
            end
        end
    end

    function Mission:tallyLoadPilot(player, pilot)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjExtractPilot:getType() then
                    if obj.param.target.name == pilot.name then
                        obj.param.loadedBy = player
                    end
                end
            end
        end
    end

    function Mission:tallyUnloadPilot(player, zonename)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjUnloadExtractedPilotOrSquad:getType() then
                    if obj.param.extractObjective.param.loadedBy == player then
                        obj.param.unloadedAt = zonename
                    end
                end
            end
        end
    end

    function Mission:tallyRecon(player, targetzone, analyzezonename)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjReconZone:getType() then
                    if obj.param.target.name == targetzone then
                        obj.param.reconData = targetzone
                    end
                end
            end
        end
    end

    function Mission:tallyLoadSquad(player, squad)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjExtractSquad:getType() then
                    if obj.param.target.name == squad.name then
                        obj.param.loadedBy = player
                    end
                end
            end
        end
    end

    function Mission:tallyUnloadSquad(player, zonename, unloadedType)
        for _,obj in ipairs(self.objectives) do
            if not obj.isComplete and not obj.isFailed then
                if obj.type == ObjUnloadExtractedPilotOrSquad:getType() then
                    if obj.param.extractObjective.param.loadedBy == player and unloadedType == PlayerLogistics.infantryTypes.extractable then
                        obj.param.unloadedAt = zonename
                    end
                elseif obj.type == ObjDeploySquad:getType() then
                    obj.param.unloadedType = unloadedType
                    obj.param.unloadedAt = zonename
                end
            end
        end
    end

    function Mission:getBriefDescription()
        local msg = '~~~~~'..self.name..' ['..self.missionID..']~~~~~\n'..self.description..'\n'

        msg = msg..' Reward:'

        for _,r in ipairs(self.rewards) do
            msg = msg..' ['..r.type..': '..r.amount..']'
        end

        return msg
    end

    function Mission:generateRewards()
        if not self.type then return end
        
        local rewardDef = RewardDefinitions.missions[self.type]
        
        self.rewards = {}
        table.insert(self.rewards, {
            type = PlayerTracker.statTypes.xp,
            amount = math.random(rewardDef.xp.low,rewardDef.xp.high)*50
        })
    end

    function Mission:getDetailedDescription()
        local msg = '['..self.name..']'

        if self.state == Mission.states.comencing or self.state == Mission.states.preping or (not Config.restrictMissionAcceptance) then
            msg = msg..'\nJoin code ['..self.missionID..']'
        end

        msg = msg..'\nReward:'

        for _,r in ipairs(self.rewards) do
            msg = msg..' ['..r.type..': '..r.amount..']'
        end
        msg = msg..'\n'

        if #self.objectives>1 then
            msg = msg..'\nObjectives: '
            if self.completionType == Mission.completion_type.all then
                msg = msg..'(Complete ALL)\n'
            elseif self.completionType == Mission.completion_type.any then
                msg = msg..'(Complete ONE)\n'
            end
        elseif #self.objectives==1 then
            msg = msg..'\nObjective: \n'
        end

        for i,v in ipairs(self.objectives) do
            local obj = v:getText()
            if v.isComplete then 
                obj = '[✓]'..obj
            elseif v.isFailed then
                obj = '[X]'..obj
            else
                obj = '[ ]'..obj 
            end

            msg = msg..'\n'..obj..'\n'
        end

        msg = msg..'\nPlayers:'
        for i,_ in pairs(self.players) do
            msg = msg..'\n  '..i
        end

        return msg
    end
end

-----------------[[ END OF Missions/Mission.lua ]]-----------------



-----------------[[ Missions/CAP_Easy.lua ]]-----------------

CAP_Easy = Mission:new()
do
    function CAP_Easy.canCreate()
        local zoneNum = 0
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront == 0 then 
                zoneNum = zoneNum + 1
            end

            if zoneNum >= 2 then return true end
        end
    end

    function CAP_Easy:getMissionName()
        return 'CAP'
    end

    function CAP_Easy:isUnitTypeAllowed(unit)
        return unit:hasAttribute('Planes')
    end

    function CAP_Easy:generateObjectives()
        self.completionType = Mission.completion_type.any
        local description = ''
        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront == 0 then
                table.insert(viableZones, zone)
            end
        end
        
        if #viableZones >= 2 then
            local choice1 = math.random(1,#viableZones)
            local zn1 = viableZones[choice1]

            local patrol1 = ObjPlayerCloseToZone:new()
            patrol1:initialize(self, {
                target = zn1,
                range = 20000,
                amount = 15*60,
                maxAmount = 15*60,
                lastUpdate = 0
            })

            table.insert(self.objectives, patrol1)
            description = description..'   Patrol airspace near '..zn1.name..'\n   OR\n'
        end

        local kills = ObjDestroyUnitsWithAttribute:new()
        kills:initialize(self, {
            attr = {'Planes', 'Helicopters'},
            amount = math.random(2,4),
            killed = 0 
        })

        table.insert(self.objectives, kills)
        description = description..'   Kill '..kills.param.amount..' aircraft'
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/CAP_Easy.lua ]]-----------------



-----------------[[ Missions/CAP_Medium.lua ]]-----------------

CAP_Medium = Mission:new()
do
    function CAP_Medium.canCreate()
        local zoneNum = 0
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront == 0 then 
                zoneNum = zoneNum + 1
            end

            if zoneNum >= 2 then return true end
        end
    end

    function CAP_Medium:getMissionName()
        return 'CAP'
    end

    function CAP_Medium:isUnitTypeAllowed(unit)
        return unit:hasAttribute('Planes')
    end

    function CAP_Medium:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront == 0 then
                table.insert(viableZones, zone)
            end
        end
        
        if #viableZones >= 2 then
            local choice1 = math.random(1,#viableZones)
            local zn1 = viableZones[choice1]
            table.remove(viableZones,choice1)
            local choice2 = math.random(1,#viableZones)
            local zn2 = viableZones[choice2]

            local patrol1 = ObjPlayerCloseToZone:new()
            patrol1:initialize(self, {
                target = zn1,
                range = 20000,
                amount = 10*60,
                maxAmount = 10*60,
                lastUpdate = 0
            })

            table.insert(self.objectives, patrol1)

            local patrol2 = ObjPlayerCloseToZone:new()
            patrol2:initialize(self, {
                target = zn2,
                range = 20000,
                amount = 10*60,
                maxAmount = 10*60,
                lastUpdate = 0
            })

            table.insert(self.objectives, patrol2)
            description = description..'   Patrol airspace near '..zn1.name..' and '..zn2.name..'\n'

            local rewardDef = RewardDefinitions.missions[self.type]

            local kills = ObjAirKillBonus:new()
            kills:initialize(self, {
                attr = {'Planes', 'Helicopters'},
                bonus = {
                    [PlayerTracker.statTypes.xp] = rewardDef.xp.boost
                },
                count = 0,
                linkedObjectives = {patrol1, patrol2}
            })

            table.insert(self.objectives, kills)
            description = description..'   Aircraft kills increase reward'
        end

        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/CAP_Medium.lua ]]-----------------



-----------------[[ Missions/CAS_Easy.lua ]]-----------------

CAS_Easy = Mission:new()
do
    function CAS_Easy.canCreate()
        return true
    end

    function CAS_Easy:getMissionName()
        return 'CAS'
    end

    function CAS_Easy:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        
        local kills = ObjDestroyUnitsWithAttribute:new()
        kills:initialize(self, {
            attr = {'Ground Units'},
            amount = math.random(3,6),
            killed = 0 
        })

        table.insert(self.objectives, kills)
        description = description..'   Destroy '..kills.param.amount..' Ground Units'
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/CAS_Easy.lua ]]-----------------



-----------------[[ Missions/CAS_Medium.lua ]]-----------------

CAS_Medium = Mission:new()
do
    function CAS_Medium.canCreate()
        return true
    end

    function CAS_Medium:getMissionName()
        return 'CAS'
    end

    function CAS_Medium:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        
        local kills = ObjDestroyUnitsWithAttribute:new()
        kills:initialize(self, {
            attr = {'Ground Units'},
            amount = math.random(8,12),
            killed = 0
        })

        table.insert(self.objectives, kills)
        description = description..'   Destroy '..kills.param.amount..' Ground Units'
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/CAS_Medium.lua ]]-----------------



-----------------[[ Missions/CAS_Hard.lua ]]-----------------

CAS_Hard = Mission:new()
do
    function CAS_Hard.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Ground Units"}, 1, 6) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    return true
                end
            end
        end
    end

    function CAS_Hard:getMissionName()
        return 'CAS'
    end

    function CAS_Hard:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront == 0 and zone:hasUnitWithAttributeOnSide({"Ground Units"}, 1, 6) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    table.insert(viableZones, zone)
                end
            end
        end

        if #viableZones == 0 then
            for _,zone in pairs(ZoneCommand.getAllZones()) do
                if zone.side == 1 and zone.distToFront == 1 and zone:hasUnitWithAttributeOnSide({"Ground Units"}, 1, 6) then
                    if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                        table.insert(viableZones, zone)
                    end
                end
            end
        end
        
        if #viableZones > 0 then
            local choice = math.random(1,#viableZones)
            local zn = viableZones[choice]
            
            local kill = ObjDestroyUnitsWithAttributeAtZone:new()
            kill:initialize(self, {
                attr = {"Ground Units"},
                amount = 1,
                killed = 0,
                tgtzone = zn
            })
            table.insert(self.objectives, kill)

            local clear = ObjClearZoneOfUnitsWithAttribute:new()
            clear:initialize(self, {
                attr = {"Ground Units"},
                tgtzone = zn
            })
            table.insert(self.objectives, clear)

            description = description..'   Clear '..zn.name..' of ground units'
            self.info = {
                targetzone = zn
            }
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/CAS_Hard.lua ]]-----------------



-----------------[[ Missions/SEAD.lua ]]-----------------

SEAD = Mission:new()
do
    function SEAD.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasSAMRadarOnSide(1) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    return true
                end
            end
        end
    end

    function SEAD:getMissionName()
        return 'SEAD'
    end

    function SEAD:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront == 0 and zone:hasSAMRadarOnSide(1) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    table.insert(viableZones, zone)
                end
            end
        end

        if #viableZones == 0 then
            for _,zone in pairs(ZoneCommand.getAllZones()) do
                if zone.side == 1 and zone.distToFront == 1 and zone:hasSAMRadarOnSide(1) then
                    if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                        table.insert(viableZones, zone)
                    end
                end
            end
        end
        
        if #viableZones > 0 then
            local choice = math.random(1,#viableZones)
            local zn = viableZones[choice]
            
            local kill = ObjDestroyUnitsWithAttributeAtZone:new()
            kill:initialize(self, {
                attr = {'SAM SR','SAM TR'},
                amount = 1,
                killed = 0,
                tgtzone = zn
            })

            table.insert(self.objectives, kill)
            description = description..'   Destroy '..kill.param.amount..' Search Radar or Track Radar at '..zn.name
            self.info = {
                targetzone = zn
            }
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/SEAD.lua ]]-----------------



-----------------[[ Missions/DEAD.lua ]]-----------------

DEAD = Mission:new()
do
    function DEAD.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Air Defence"}, 1, 4) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    return true
                end
            end
        end
    end

    function DEAD:getMissionName()
        return 'DEAD'
    end

    function DEAD:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Air Defence"}, 1, 4) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    table.insert(viableZones, zone)
                end
            end
        end
        
        if #viableZones > 0 then
            local choice = math.random(1,#viableZones)
            local zn = viableZones[choice]
            
            local kill = ObjDestroyUnitsWithAttributeAtZone:new()
            kill:initialize(self, {
                attr = {"Air Defence"},
                amount = 1,
                killed = 0,
                tgtzone = zn
            })
            table.insert(self.objectives, kill)

            local clear = ObjClearZoneOfUnitsWithAttribute:new()
            clear:initialize(self, {
                attr = {"Air Defence"},
                tgtzone = zn
            })
            table.insert(self.objectives, clear)

            description = description..'   Clear '..zn.name..' of any Air Defenses'
            self.info = {
                targetzone = zn
            }
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/DEAD.lua ]]-----------------



-----------------[[ Missions/Supply_Easy.lua ]]-----------------

Supply_Easy = Mission:new()
do
    function Supply_Easy.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront and zone.distToFront <=1 and zone:criticalOnSupplies() then
                return true
            end
        end
    end

    function Supply_Easy:getMissionName()
        return "Supply delivery"
    end

    function Supply_Easy:isInstantReward()
        return true
    end

    function Supply_Easy:isUnitTypeAllowed(unit)
        if PlayerLogistics then
            local unitType = unit:getDesc()['typeName']
            return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].supplies
        end
    end

    function Supply_Easy:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''

        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront <=1 and zone:criticalOnSupplies() then
                table.insert(viableZones, zone)
            end
        end
        
        if #viableZones > 0 then
            local choice = math.random(1,#viableZones)
            local zn = viableZones[choice]
            
            local deliver = ObjSupplyZone:new()
            deliver:initialize(self, {
                amount = math.random(2,6)*250,
                delivered = 0,
                tgtzone = zn
            })
            
            table.insert(self.objectives, deliver)
            description = description..'   Deliver '..deliver.param.amount..' of supplies to '..zn.name
            self.info = {
                targetzone = zn
            }
        end

        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Supply_Easy.lua ]]-----------------



-----------------[[ Missions/Supply_Hard.lua ]]-----------------

Supply_Hard = Mission:new()
do
    function Supply_Hard.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront and zone.distToFront <=1 and zone:criticalOnSupplies() then
                return true
            end
        end
    end

    function Supply_Hard:getMissionName()
        return "Supply delivery"
    end

    function Supply_Hard:isInstantReward()
        return true
    end

    function Supply_Hard:isUnitTypeAllowed(unit)
        if PlayerLogistics then
            local unitType = unit:getDesc()['typeName']
            return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].supplies
        end
    end

    function Supply_Hard:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''

        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 2 and zone.distToFront == 0 and zone:criticalOnSupplies() then
                table.insert(viableZones, zone)
            end
        end

        if #viableZones == 0 then
            for _,zone in pairs(ZoneCommand.getAllZones()) do
                if zone.side == 2 and zone.distToFront == 1 and zone:criticalOnSupplies() then
                    table.insert(viableZones, zone)
                end
            end
        end
        
        if #viableZones > 0 then
            local choice = math.random(1,#viableZones)
            local zn = viableZones[choice]
            
            local deliver = ObjSupplyZone:new()
            deliver:initialize(self, {
                amount = math.random(18,24)*250,
                delivered = 0,
                tgtzone = zn
            })
            
            table.insert(self.objectives, deliver)
            description = description..'   Deliver '..deliver.param.amount..' of supplies to '..zn.name
            self.info = {
                targetzone = zn
            }
        end

        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Supply_Hard.lua ]]-----------------



-----------------[[ Missions/Strike_VeryEasy.lua ]]-----------------

Strike_VeryEasy = Mission:new()
do
    function Strike_VeryEasy.canCreate()
        return true
    end

    function Strike_VeryEasy:getMissionName()
        return 'Strike'
    end

    function Strike_VeryEasy:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        
        local kills = ObjDestroyUnitsWithAttribute:new()
        kills:initialize(self, {
            attr = {'Buildings'},
            amount = 1,
            killed = 0 
        })

        table.insert(self.objectives, kills)
        description = description..'   Destroy '..kills.param.amount..' building'
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Strike_VeryEasy.lua ]]-----------------



-----------------[[ Missions/Strike_Easy.lua ]]-----------------

Strike_Easy = Mission:new()
do
    function Strike_Easy.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({'Buildings'}, 1) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    return true
                end
            end
        end
    end

    function Strike_Easy:getMissionName()
        return 'Strike'
    end

    function Strike_Easy:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront == 0 and zone:hasUnitWithAttributeOnSide({'Buildings'}, 1) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    table.insert(viableZones, zone)
                end
            end
        end

        if #viableZones == 0 then
            for _,zone in pairs(ZoneCommand.getAllZones()) do
                if zone.side == 1 and zone.distToFront == 1 and zone:hasUnitWithAttributeOnSide({'Buildings'}, 1) then
                    if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                        table.insert(viableZones, zone)
                    end
                end
            end
        end
        
        if #viableZones > 0 then
            local choice = math.random(1,#viableZones)
            local zn = viableZones[choice]
            
            local kill = ObjDestroyUnitsWithAttributeAtZone:new()
            kill:initialize(self, {
                attr = {'Buildings'},
                amount = 1,
                killed = 0,
                tgtzone = zn
            })

            table.insert(self.objectives, kill)
            description = description..'   Destroy '..kill.param.amount..' Building at '..zn.name
            self.info = {
                targetzone = zn
            }
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Strike_Easy.lua ]]-----------------



-----------------[[ Missions/Strike_Medium.lua ]]-----------------

Strike_Medium = Mission:new()
do
    function Strike_Medium.canCreate()
        return MissionTargetRegistry.strikeTargetsAvailable(1, false)
    end

    function Strike_Medium:getMissionName()
        return 'Strike'
    end

    function Strike_Medium:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
       
        local tgt = MissionTargetRegistry.getRandomStrikeTarget(1, false)
        
        if tgt then
            local chozenTarget = tgt.data
            local zn = tgt.zone

            local kill = ObjDestroyStructure:new()
            kill:initialize(self, {
                target = chozenTarget,
                tgtzone = zn,
                killed = false
            })

            table.insert(self.objectives, kill)
            description = description..'   Destroy '..chozenTarget.display..' at '..zn.name
            self.info = {
                targetzone = zn
            }

        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Strike_Medium.lua ]]-----------------



-----------------[[ Missions/Strike_Hard.lua ]]-----------------

Strike_Hard = Mission:new()
do
    function Strike_Hard.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Buildings"}, 1, 3) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    return true
                end
            end
        end
    end

    function Strike_Hard:getMissionName()
        return 'Strike'
    end

    function Strike_Hard:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront == 0 and zone:hasUnitWithAttributeOnSide({"Buildings"}, 1, 3) then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    table.insert(viableZones, zone)
                end
            end
        end

        if #viableZones == 0 then
            for _,zone in pairs(ZoneCommand.getAllZones()) do
                if zone.side == 1 and zone.distToFront == 1 and zone:hasUnitWithAttributeOnSide({"Buildings"}, 1, 3) then
                    if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                        table.insert(viableZones, zone)
                    end
                end
            end
        end
        
        if #viableZones > 0 then
            local choice = math.random(1,#viableZones)
            local zn = viableZones[choice]
            
            local kill = ObjDestroyUnitsWithAttributeAtZone:new()
            kill:initialize(self, { 
                attr = {"Buildings"},
                amount = 1,
                killed = 0,
                tgtzone = zn
            })

            table.insert(self.objectives, kill)

            local clear = ObjClearZoneOfUnitsWithAttribute:new()
            clear:initialize(self, {
                attr = {"Buildings"},
                tgtzone = zn
            })
            table.insert(self.objectives, clear)

            description = description..'   Destroy every structure at '..zn.name
            self.info = {
                targetzone = zn
            }
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Strike_Hard.lua ]]-----------------



-----------------[[ Missions/Deep_Strike.lua ]]-----------------

Deep_Strike = Mission:new()
do
    function Deep_Strike.canCreate()
        return MissionTargetRegistry.strikeTargetsAvailable(1, true)
    end

    function Deep_Strike:getMissionName()
        return 'Deep Strike'
    end

    function Deep_Strike:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
       
        local tgt = MissionTargetRegistry.getRandomStrikeTarget(1, true)
        
        if tgt then
            local chozenTarget = tgt.data
            local zn = tgt.zone

            local kill = ObjDestroyStructure:new()
            kill:initialize(self, {
                target = chozenTarget,
                tgtzone = zn,
                killed = false
            })

            table.insert(self.objectives, kill)
            description = description..'   Destroy '..chozenTarget.display..' at '..zn.name
            self.info = {
                targetzone = zn
            }

        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Deep_Strike.lua ]]-----------------



-----------------[[ Missions/Escort.lua ]]-----------------

Escort = Mission:new()
do    
    function Escort.canCreate()
        local currentTime = timer.getAbsTime()
        for _,gr in pairs(DependencyManager.get("GroupMonitor").groups) do
            if gr.product.side == 2 and gr.product.type == 'mission' and (gr.product.missionType == 'supply_convoy' or gr.product.missionType == 'assault')  then
                local z = gr.target
                if z.distToFront == 0 and z.side~= 2 then
                    if gr.state == nil or gr.state == 'started' or (gr.state == 'enroute' and (currentTime - gr.lastStateTime < 7*60)) then
                        return true
                    end
                end
            end
        end
    end

    function Escort:getMissionName()
        return "Escort convoy"
    end

    function Escort:isUnitTypeAllowed(unit)
        return unit:hasAttribute('Helicopters')
    end

    function Escort:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''

        local currentTime = timer.getAbsTime()
        local viableConvoys = {}
        for _,gr in pairs(DependencyManager.get("GroupMonitor").groups) do
            if gr.product.side == 2 and gr.product.type == 'mission' and (gr.product.missionType == 'supply_convoy' or gr.product.missionType == 'assault')  then
                local z = gr.target
                if z.distToFront == 0 and z.side ~= 2 then
                    if gr.state == nil or gr.state == 'started' or (gr.state == 'enroute' and (currentTime - gr.lastStateTime < 7*60)) then
                        table.insert(viableConvoys, gr)
                    end
                end
            end
        end
        
        if #viableConvoys > 0 then
            local choice = math.random(1,#viableConvoys)
            local convoy = viableConvoys[choice]
            
            local escort = ObjEscortGroup:new()
            escort:initialize(self, {
                maxAmount = 60*7,
                amount = 60*7,
                proxDist = 400,
                target = convoy,
                lastUpdate = timer.getAbsTime()
            })
            
            table.insert(self.objectives, escort)

            local nearzone = ""
            local gr = Group.getByName(convoy.name)
            if gr and gr:getSize()>0 then
                local un = gr:getUnit(1)
                local closest = ZoneCommand.getClosestZoneToPoint(un:getPoint())
                if closest then
                    nearzone = ' near '..closest.name..''
                end
            end

            description = description..'   Escort convoy'..nearzone..' on route to their destination'
            --description = description..'\n   Target will be assigned after accepting mission'

        end

        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Escort.lua ]]-----------------



-----------------[[ Missions/TARCAP.lua ]]-----------------

TARCAP = Mission:new()
do
    TARCAP.relevantMissions = {
        Mission.types.cas_hard,
        Mission.types.dead,
        Mission.types.sead,
        Mission.types.strike_easy,
        Mission.types.strike_hard
    }

    function TARCAP:new(id, type, activeMissions)
        self = Mission.new(self, id, type)
        self:generateObjectivesOverload(activeMissions)
		return self
	end

    function TARCAP.canCreate(activeMissions)
        for _,mis in pairs(activeMissions) do
            for _,tp in ipairs(TARCAP.relevantMissions) do
                if mis.type == tp then return true end
            end
        end
    end

    function TARCAP:getMissionName()
        return 'TARCAP'
    end

    function TARCAP:isUnitTypeAllowed(unit)
        return unit:hasAttribute('Planes')
    end

    function TARCAP:generateObjectivesOverload(activeMissions)
        self.completionType = Mission.completion_type.any
        local description = ''
        local viableMissions = {}
        for _,mis in pairs(activeMissions) do
            for _,tp in ipairs(TARCAP.relevantMissions) do
                if mis.type == tp then
                    table.insert(viableMissions, mis)
                    break
                end
            end
        end
        
        if #viableMissions >= 1 then
            local choice = math.random(1,#viableMissions)
            local mis = viableMissions[choice]

            local protect = ObjProtectMission:new()
            protect:initialize(self, {
                mis = mis
            })

            table.insert(self.objectives, protect)
            description = description..'   Prevent enemy aircraft from interfering with friendly '..mis:getMissionName()..' mission'
            if mis.info and mis.info.targetzone then
                description = description..' at '..mis.info.targetzone.name
            end

            local rewardDef = RewardDefinitions.missions[self.type]

            local kills = ObjAirKillBonus:new()
            kills:initialize(self, {
                attr = {'Planes'},
                bonus = {
                    [PlayerTracker.statTypes.xp] = rewardDef.xp.boost
                },
                count = 0,
                linkedObjectives = {protect}
            })
    
            table.insert(self.objectives, kills)
            
            description = description..'\n   Aircraft kills increase reward'
        end

        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/TARCAP.lua ]]-----------------



-----------------[[ Missions/Recon.lua ]]-----------------

Recon = Mission:new()
do
    function Recon.canCreate()
        local zoneNum = 0
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront == 0 and zone.revealTime == 0 then 
                return true
            end
        end
    end

    function Recon:getMissionName()
        return 'Recon'
    end

    function Recon:isUnitTypeAllowed(unit)
        return true
    end

    function Recon:isInstantReward()
        return true
    end

    function Recon:generateObjectives()
        self.completionType = Mission.completion_type.any
        local description = ''
        local viableZones = {}
        local secondaryViableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront == 0 and zone.revealTime == 0 then
                table.insert(viableZones, zone)
            end
        end

        if #viableZones > 0 then
            local choice1 = math.random(1,#viableZones)
            local zn1 = viableZones[choice1]

            local recon = ObjReconZone:new()
            recon:initialize(self, {
                target = zn1,
                failZones = {
                    [1] = {zn1}
                }
            })

            table.insert(self.objectives, recon)
            description = description..'   Observe enemies at '..zn1.name..'\n'
        end

        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Recon.lua ]]-----------------



-----------------[[ Missions/BAI.lua ]]-----------------

BAI = Mission:new()
do
    function BAI.canCreate()
        return MissionTargetRegistry.baiTargetsAvailable(1)
    end

    function BAI:getMissionName()
        return 'BAI'
    end

    function BAI:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
       
        local tgt = MissionTargetRegistry.getRandomBaiTarget(1)
        
        if tgt then

            local gr = Group.getByName(tgt.name)
            if gr and gr:getSize()>0 then
                local units = {}
                for i,v in ipairs(gr:getUnits()) do
                    units[v:getName()] = false
                end

                local kill = ObjDestroyGroup:new() 
                kill:initialize(self, {
                    target = tgt,
                    targetUnitNames = units,
                    lastUpdate = timer.getAbsTime()
                })

                table.insert(self.objectives, kill)

                local nearzone = ""
                local un = gr:getUnit(1)
                local closest = ZoneCommand.getClosestZoneToPoint(un:getPoint())
                if closest then
                    nearzone = ' near '..closest.name..''
                end

                description = description..'   Destroy '..tgt.product.display..nearzone..' before it reaches its destination.'
            end

            MissionTargetRegistry.removeBaiTarget(tgt)
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/BAI.lua ]]-----------------



-----------------[[ Missions/Anti_Runway.lua ]]-----------------

Anti_Runway = Mission:new()
do
    function Anti_Runway.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront <=2 and zone:hasRunway() then
                    return true
            end
        end
    end

    function Anti_Runway:getMissionName()
        return 'Runway Attack'
    end

    function Anti_Runway:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        local viableZones = {}
       
        local tgts = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.side == 1 and zone.distToFront <=2 and zone:hasRunway() then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    table.insert(tgts, zone)
                end
            end
        end
        
        if #tgts > 0 then
            local tgt = tgts[math.random(1,#tgts)]

            local rewardDef = RewardDefinitions.missions[self.type]

            local bomb = ObjBombInsideZone:new()
            bomb:initialize(self,{
                targetZone = tgt,
                max = 20,
                required = 5,
                dropped = 0,
                isFinishStarted = false,
                bonus = {
                    [PlayerTracker.statTypes.xp] = rewardDef.xp.boost
                }
            })

            table.insert(self.objectives, bomb)
            description = description..'   Bomb runway at '..bomb.param.targetZone.name
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Anti_Runway.lua ]]-----------------



-----------------[[ Missions/CSAR.lua ]]-----------------

CSAR = Mission:new()
do
    function CSAR.canCreate()
        return MissionTargetRegistry.pilotsAvailableToExtract()
    end

    function CSAR:getMissionName()
        return 'CSAR'
    end

    function CSAR:isInstantReward()
        return true
    end

    function CSAR:isUnitTypeAllowed(unit)
        if PlayerLogistics then
            local unitType = unit:getDesc()['typeName']
            return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].personCapacity and PlayerLogistics.allowedTypes[unitType].personCapacity > 0
        end
    end

    function CSAR:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        
        if MissionTargetRegistry.pilotsAvailableToExtract() then
            local tgt = MissionTargetRegistry.getRandomPilot()
            
            local extract = ObjExtractPilot:new()
            extract:initialize(self, {
                target = tgt,
                loadedBy = nil,
                lastUpdate = timer.getAbsTime()
            })
            table.insert(self.objectives, extract)

            local unload = ObjUnloadExtractedPilotOrSquad:new()
            unload:initialize(self, {
                extractObjective = extract
            })
            table.insert(self.objectives, unload)

            local nearzone = ''
            local closest = ZoneCommand.getClosestZoneToPoint(tgt.pilot:getUnit(1):getPoint())
            if closest then
                nearzone = ' near '..closest.name..''
            end

            description = description..'   Rescue '..tgt.name..nearzone..' and deliver them to a friendly zone'
            --local mgrs = coord.LLtoMGRS(coord.LOtoLL(tgt.pilot:getUnit(1):getPoint()))
            --local grid = mist.tostringMGRS(mgrs, 2):gsub(' ','')
            --description = description..' ['..grid..']'
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/CSAR.lua ]]-----------------



-----------------[[ Missions/Extraction.lua ]]-----------------

Extraction = Mission:new()
do
    function Extraction.canCreate()
        return MissionTargetRegistry.squadsReadyToExtract(2)
    end

    function Extraction:getMissionName()
        return 'Extraction'
    end

    function Extraction:isInstantReward()
        return true
    end
    
    function Extraction:isUnitTypeAllowed(unit)
        if PlayerLogistics then
            local unitType = unit:getDesc()['typeName']
            return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].personCapacity
        end
    end

    function Extraction:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''
        
        if MissionTargetRegistry.squadsReadyToExtract(2) then
            local tgt = MissionTargetRegistry.getRandomSquad(2)
            if tgt then
                local extract = ObjExtractSquad:new()
                extract:initialize(self, {
                    target = tgt,
                    loadedBy = nil,
                    lastUpdate = timer.getAbsTime()
                })
                table.insert(self.objectives, extract)

                local unload = ObjUnloadExtractedPilotOrSquad:new()
                unload:initialize(self, {
                    extractObjective = extract
                })
                table.insert(self.objectives, unload)

                local infName = PlayerLogistics.getInfantryName(tgt.data.type)

                
                local nearzone = ''
                local gr = Group.getByName(tgt.name)
                if gr and gr:isExist() and gr:getSize()>0 then
                    local un = gr:getUnit(1)
                    local closest = ZoneCommand.getClosestZoneToPoint(un:getPoint())
                    if closest then
                        nearzone = ' near '..closest.name..''
                    end
                    --local mgrs = coord.LLtoMGRS(coord.LOtoLL(un:getPoint()))
                    --local grid = mist.tostringMGRS(mgrs, 2):gsub(' ','')
                    --description = description..' ['..grid..']'
                end

                description = description..'   Extract '..infName..nearzone..' to a friendly zone'
            end
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/Extraction.lua ]]-----------------



-----------------[[ Missions/DeploySquad.lua ]]-----------------

DeploySquad = Mission:new()
do
    function DeploySquad.canCreate()
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.distToFront and zone.distToFront == 0 then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    return true
                end
            end
        end
    end

    function DeploySquad:getMissionName()
        return 'Deploy infantry'
    end

    function DeploySquad:isInstantReward()
        local friendlyDeployments = {
            [PlayerLogistics.infantryTypes.engineer] = true,
        }

        if self.objectives and self.objectives[1] then
            local sqType = self.objectives[1].param.squadType
            if friendlyDeployments[sqType] then
                return true
            end
        end

        return false
    end
    
    function DeploySquad:isUnitTypeAllowed(unit)
        if PlayerLogistics then
            local unitType = unit:getDesc()['typeName']
            return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].personCapacity
        end
    end

    function DeploySquad:generateObjectives()
        self.completionType = Mission.completion_type.all
        local description = ''

        local viableZones = {}
        for _,zone in pairs(ZoneCommand.getAllZones()) do
            if zone.distToFront and zone.distToFront == 0 then
                if not MissionTargetRegistry.isZoneTargeted(zone.name) then
                    table.insert(viableZones, zone)
                end
            end
        end

        if #viableZones > 0 then
            local tgt = viableZones[math.random(1,#viableZones)]
            if tgt then
                local squadType = nil

                if tgt.side == 0 then
                    squadType = PlayerLogistics.infantryTypes.capture
                elseif tgt.side == 1 then
                    if math.random()>0.5 then
                        squadType = PlayerLogistics.infantryTypes.sabotage
                    else
                        squadType = PlayerLogistics.infantryTypes.spy
                    end
                elseif tgt.side == 2 then
                    squadType = PlayerLogistics.infantryTypes.engineer
                end

                local deploy = ObjDeploySquad:new()
                deploy:initialize(self, {
                    squadType = squadType,
                    targetZone = tgt,
                    requiredZoneSide = tgt.side,
                    unloadedType = nil,
                    unloadedAt = nil
                })
                table.insert(self.objectives, deploy)

                local infName = PlayerLogistics.getInfantryName(squadType)

                description = description..'   Deploy '..infName..' to '..tgt.name
                
                self.info = {
                    targetzone = tgt
                }
            end
        end
        self.description = self.description..description
    end
end

-----------------[[ END OF Missions/DeploySquad.lua ]]-----------------



-----------------[[ RewardDefinitions.lua ]]-----------------

RewardDefinitions = {}

do
    RewardDefinitions.missions = {
      [Mission.types.cap_easy]        = { xp = { low = 10, high = 20, boost = 0   } },
      [Mission.types.cap_medium]      = { xp = { low = 10, high = 20, boost = 100 } },
      [Mission.types.tarcap]          = { xp = { low = 10, high = 10, boost = 150 } },
      [Mission.types.cas_easy]        = { xp = { low = 10, high = 20, boost = 0   } },
      [Mission.types.cas_medium]      = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.cas_hard]        = { xp = { low = 30, high = 40, boost = 0   } },
      [Mission.types.bai]             = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.sead]            = { xp = { low = 10, high = 20, boost = 0   } },
      [Mission.types.dead]            = { xp = { low = 30, high = 40, boost = 0   } },
      [Mission.types.strike_veryeasy] = { xp = { low = 5,  high = 10, boost = 0   } },
      [Mission.types.strike_easy]     = { xp = { low = 10, high = 20, boost = 0   } },
      [Mission.types.strike_medium]   = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.strike_hard]     = { xp = { low = 30, high = 40, boost = 0   } },
      [Mission.types.deep_strike]     = { xp = { low = 30, high = 40, boost = 0   } },
      [Mission.types.anti_runway]     = { xp = { low = 20, high = 30, boost = 25  } },
      [Mission.types.supply_easy]     = { xp = { low = 10, high = 20, boost = 0   } },
      [Mission.types.supply_hard]     = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.escort]          = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.recon]           = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.csar]            = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.extraction]      = { xp = { low = 20, high = 30, boost = 0   } },
      [Mission.types.deploy_squad]    = { xp = { low = 20, high = 30, boost = 0   } }
    }

    RewardDefinitions.actions = {
      pilotExtract = 100,
      squadDeploy = 150,
      squadExtract = 150,
      supplyRatio = 0.06,
      supplyBoost = 0.5,
      recon = 150
    }
end

-----------------[[ END OF RewardDefinitions.lua ]]-----------------



-----------------[[ MissionTracker.lua ]]-----------------

MissionTracker = {}
do
    MissionTracker.maxMissionCount = {
        [Mission.types.cap_easy] = 2,
        [Mission.types.cap_medium] = 1,
        [Mission.types.cas_easy] = 2,
        [Mission.types.cas_medium] = 1,
        [Mission.types.cas_hard] = 1,
        [Mission.types.sead] = 3,
        [Mission.types.supply_easy] = 3,
        [Mission.types.supply_hard] = 1,
        [Mission.types.strike_veryeasy] = 2,
        [Mission.types.strike_easy] = 1,
        [Mission.types.strike_medium] = 3,
        [Mission.types.strike_hard] = 1,
        [Mission.types.dead] = 1,
        [Mission.types.escort] = 2,
        [Mission.types.tarcap] = 1,
        [Mission.types.deep_strike] = 3,
        [Mission.types.recon] = 3,
        [Mission.types.bai] = 1,
        [Mission.types.anti_runway] = 2,
        [Mission.types.csar] = 1,
        [Mission.types.extraction] = 1,
        [Mission.types.deploy_squad] = 3,
    }

    if Config.missions then
        for i,v in pairs(Config.missions) do
            if MissionTracker.maxMissionCount[i] then
                MissionTracker.maxMissionCount[i] = v
            end
        end
    end

    MissionTracker.missionBoardSize = Config.missionBoardSize or 15

	function MissionTracker:new()
		local obj = {}
        obj.groupMenus = {}
        obj.missionIDPool = {}
        obj.missionBoard = {}
        obj.activeMissions = {}
		
		setmetatable(obj, self)
		self.__index = self

        DependencyManager.get("MarkerCommands"):addCommand('list', function(event, _, state) 
            if event.initiator then
                state:printMissionBoard(event.initiator:getID(), nil, event.initiator:getGroup():getName())
            elseif world.getPlayer() then
                local unit = world.getPlayer()
                state:printMissionBoard(unit:getID(), nil, event.initiator:getGroup():getName())
            end
            return true
        end, nil, obj)

        DependencyManager.get("MarkerCommands"):addCommand('help', function(event, _, state) 
            if event.initiator then
                state:printHelp(event.initiator:getID())
            elseif world.getPlayer() then
                local unit = world.getPlayer()
                state:printHelp(unit:getID())
            end
            return true
        end, nil, obj)

        DependencyManager.get("MarkerCommands"):addCommand('active', function(event, _, state) 
            if event.initiator then
                state:printActiveMission(event.initiator:getID(), nil, event.initiator:getPlayerName())
            elseif world.getPlayer() then
                state:printActiveMission(nil, nil, world.getPlayer():getPlayerName())
            end
            return true
        end, nil, obj)

        DependencyManager.get("MarkerCommands"):addCommand('accept',function(event, code, state) 
            local numcode = tonumber(code)
            if not numcode or numcode<1000 or numcode > 9999 then return false end

            local player = ''
            local unit = nil
            if event.initiator then 
                player = event.initiator:getPlayerName()
                unit = event.initiator
            elseif world.getPlayer() then
                player = world.getPlayer():getPlayerName()
                unit = world.getPlayer()
            end

            return state:activateMission(numcode, player, unit)
        end, true, obj)

        DependencyManager.get("MarkerCommands"):addCommand('join',function(event, code, state) 
            local numcode = tonumber(code)
            if not numcode or numcode<1000 or numcode > 9999 then return false end

            local player = ''
            local unit = nil
            if event.initiator then 
                player = event.initiator:getPlayerName()
                unit = event.initiator
            elseif world.getPlayer() then
                player = world.getPlayer():getPlayerName()
                unit = world.getPlayer()
            end

            return state:joinMission(numcode, player, unit)
        end, true, obj)

        DependencyManager.get("MarkerCommands"):addCommand('leave',function(event, _, state) 
            local player = ''
            if event.initiator then 
                player = event.initiator:getPlayerName()
            elseif world.getPlayer() then
                player = world.getPlayer():getPlayerName()
            end

            return state:leaveMission(player)
        end, nil, obj)

        obj:menuSetup()
		obj:start()
        
		DependencyManager.register("MissionTracker", obj)
		return obj
	end

    function MissionTracker:menuSetup()
        MenuRegistry:register(2, function(event, context)
            if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
				local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
                    local groupname = event.initiator:getGroup():getName()

                    if context.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
                        context.groupMenus[groupid] = nil
                    end

                    if not context.groupMenus[groupid] then
                        local menu = missionCommands.addSubMenuForGroup(groupid, 'Missions')
                        missionCommands.addCommandForGroup(groupid, 'List Missions', menu, Utils.log(context.printMissionBoard), context, nil, groupid, groupname)
                        missionCommands.addCommandForGroup(groupid, 'Active Mission', menu, Utils.log(context.printActiveMission), context, nil, groupid, nil, groupname)
                        
                        local dial = missionCommands.addSubMenuForGroup(groupid, 'Dial Code', menu)
                        for i1=1,5,1 do
                            local digit1 = missionCommands.addSubMenuForGroup(groupid, i1..'___', dial)
                            for i2=1,5,1 do
                                local digit2 = missionCommands.addSubMenuForGroup(groupid, i1..i2..'__', digit1)
                                for i3=1,5,1 do
                                    local digit3 = missionCommands.addSubMenuForGroup(groupid, i1..i2..i3..'_', digit2)
                                    for i4=1,5,1 do
                                        local code = tonumber(i1..i2..i3..i4)
                                        local digit4 = missionCommands.addCommandForGroup(groupid, i1..i2..i3..i4, digit3, Utils.log(context.activateOrJoinMissionForGroup), context, code, groupname)
                                    end
                                end
                            end
                        end
                        
                        local leavemenu = missionCommands.addSubMenuForGroup(groupid, 'Leave Mission', menu)
                        missionCommands.addCommandForGroup(groupid, 'Confirm to leave mission', leavemenu, Utils.log(context.leaveMission), context, player)
                        missionCommands.addCommandForGroup(groupid, 'Cancel', leavemenu, function() end)

                        missionCommands.addCommandForGroup(groupid, 'Help', menu, Utils.log(context.printHelp), context, nil, groupid)
                        
                        context.groupMenus[groupid] = menu
                    end
				end
			elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
					
					if context.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
                        context.groupMenus[groupid] = nil
                    end
				end
            end
        end, self)
    end

    function MissionTracker:printHelp(unitid, groupid)
        local msg = 'Missions can only be accepted or joined while landed at a friendly zone.\n'
        msg = msg.. 'Rewards from mission completion need to be claimed by landing at a friendly zone.\n\n'
        msg = msg.. 'Accept mission:\n'
        msg = msg.. ' Each mission has a 4 digit code listed next to its name.\n To accept a mission, either dial its code from the mission radio menu,\n or create a marker on the map and set its text to:\n'
        msg = msg.. '   accept:code\n'
        msg = msg.. ' (ex. accept:4126)\n\n'
        msg = msg.. 'Join mission:\n'
        msg = msg.. ' You can team up with other players, by joining a mission they already accepted.\n'
        msg = msg.. ' Missions can only be joined if all players who are already part of that mission\n have not taken off yet.\n'
        msg = msg.. ' When a mission is completed each player has to land to claim their reward individually.\n'
        msg = msg.. ' To join a mission, ask for the join code from a player who is already part of the mission,\n dial it in from the mission radio menu,\n or create a marker on the map and set its text to:\n'
        msg = msg.. '   join:code\n'
        msg = msg.. ' (ex. join:4126)\n\n'
        msg = msg.. 'Map marker commands:\n'
        msg = msg.. ' list - displays mission board\n'
        msg = msg.. ' accept:code - accepts mission with corresponding code\n'
        msg = msg.. ' join:code - joins other players mission with corresponding code\n'
        msg = msg.. ' active - displays active mission\n'
        msg = msg.. ' leave - leaves active mission\n'
        msg = msg.. ' help - displays this message'

        if unitid then
            trigger.action.outTextForUnit(unitid, msg, 30)
        elseif groupid then
            trigger.action.outTextForGroup(groupid, msg, 30)
        else
            --trigger.action.outText(msg, 30)
        end
    end

    function MissionTracker:printActiveMission(unitid, groupid, playername, groupname)
        if not playername and groupname then
            env.info('MissionTracker - printActiveMission: '..tostring(groupname)..' requested group print.')
            local gr = Group.getByName(groupname)
            for i,v in ipairs(gr:getUnits()) do
                if v.getPlayerName and v:getPlayerName() then 
                    self:printActiveMission(v:getID(), gr:getID(), v:getPlayerName())
                end
            end
            return
        end

        local mis = nil
        for i,v in pairs(self.activeMissions) do
            for pl,un in pairs(v.players) do
                if pl == playername then
                    mis = v
                    break
                end
            end

            if mis then break end
        end

        local msg = ''
        if mis then
            msg = mis:getDetailedDescription()
        else
            msg = 'No active mission'
        end

        if unitid then
            trigger.action.outTextForUnit(unitid, msg, 30)
        elseif groupid then
            trigger.action.outTextForGroup(groupid, msg, 30)
        else
            --trigger.action.outText(msg, 30)
        end
    end

    function MissionTracker:printMissionBoard(unitid, groupid, groupname)
        local gr = Group.getByName(groupname)
        local un = gr:getUnit(1)

        local msg = 'Mission Board\n'
        local empty = true
        local invalidCount = 0
        for i,v in pairs(self.missionBoard) do
            if v:isUnitTypeAllowed(un) then
                empty = false
                msg = msg..'\n'..v:getBriefDescription()..'\n'
            else
                invalidCount = invalidCount + 1
            end
        end

        if empty then 
            msg = msg..'\n No missions available'
        end

        if invalidCount > 0 then
            msg = msg..'\n'..invalidCount..' additional missions are not compatible with current aircraft\n'
        end

        if unitid then
            trigger.action.outTextForUnit(unitid, msg, 30)
        elseif groupid then
            trigger.action.outTextForGroup(groupid, msg, 30)
        else
            --trigger.action.outText(msg, 30)
        end
    end

    function MissionTracker:getNewMissionID()
        if #self.missionIDPool == 0 then
            for i=1111,5555,1 do 
                if not tostring(i):find('[06789]') then
                    if not self.missionBoard[i] and not self.activeMissions[i] then
                        table.insert(self.missionIDPool, i)
                    end
                end
            end
        end
        
        local choice = math.random(1,#self.missionIDPool)
        local newId = self.missionIDPool[choice]
        table.remove(self.missionIDPool,choice)
        return newId
    end
	
	function MissionTracker:start()
        timer.scheduleFunction(function(params, time)
            for i,v in ipairs(coalition.getPlayers(2)) do
                if v and v:isExist() and not Utils.isInAir(v) and v.getPlayerName and v:getPlayerName() then
                    local player = v:getPlayerName()
                    local cfg = DependencyManager.get("PlayerTracker"):getPlayerConfig(player)
                    if cfg.noMissionWarning == true then
                        local hasMis = false
                        for _,mis in pairs(params.context.activeMissions) do
                            if mis.players[player] then
                                hasMis = true
                                break
                            end
                        end

                        if not hasMis then
                            trigger.action.outTextForUnit(v:getID(), "No mission selected", 9)
                        end
                    end
                end
            end

            return time+10
        end, {context = self}, timer.getTime()+10)

        timer.scheduleFunction(function(param, time)
            for code,mis in pairs(param.missionBoard) do
                if timer.getAbsTime() - mis.lastStateTime > mis.expireTime then
                    param.missionBoard[code].state = Mission.states.failed
                    param.missionBoard[code] = nil
                    env.info('Mission code'..code..' expired.')
                else
                    mis:updateIsFailed()
                    if mis.state == Mission.states.failed then
                        param.missionBoard[code]=nil
                        env.info('Mission code'..code..' canceled due to objectives failed')
                        trigger.action.outTextForCoalition(2,'Mission ['..mis.missionID..'] '..mis.name..' was cancelled',5)
                    end
                end
            end

            local misCount = Utils.getTableSize(param.missionBoard)
            local toGen = MissionTracker.missionBoardSize-misCount
            if toGen > 0 then
                local validMissions = {}
                for _,v in pairs(Mission.types) do
                    if self:canCreateMission(v) then
                        table.insert(validMissions,v)
                    end
                end

                if #validMissions > 0  then
                    for i=1,toGen,1 do
                        if #validMissions > 0 then
                            local choice = math.random(1,#validMissions)
                            local misType = validMissions[choice]
                            table.remove(validMissions, choice)
                            param:generateMission(misType)
                        else
                            break
                        end
                    end
                end
            end

            return time+1
        end, self, timer.getTime()+1)

        timer.scheduleFunction(function(param, time)
            for code,mis in pairs(param.activeMissions) do
                -- check if players exist and in same unit as when joined
                -- remove from mission if false
                for pl,un in pairs(mis.players) do
                    if not un or
                        not un:isExist() then

                        mis:removePlayer(pl)
                        env.info('Mission code'..code..' removing player '..pl..', unit no longer exists')
                    end
                end
                
                -- check if mission has 0 players, delete mission if true
                if not mis:hasPlayers() then
                    param.activeMissions[code]:updateState(Mission.states.failed)
                    param.activeMissions[code] = nil
                    env.info('Mission code'..code..' canceled due to no players')
                else
                    --check if mission objectives can still be completed, cancel mission if not
                    mis:updateIsFailed()
                    mis:updateIsCompleted()

                    if mis.state == Mission.states.preping then
                        --check if any player in air and move to comencing if true
                        for pl,un in pairs(mis.players) do
                            if Utils.isInAir(un) then
                                mis:updateState(Mission.states.comencing)
                                mis:pushMessageToPlayers(mis.name..' mission is starting')
                                break
                            end
                        end
                    elseif mis.state == Mission.states.comencing then
                        --check if all players in air and move to active if true
                        --if all players landed, move to preping
                        local allInAir = true
                        local allLanded = true
                        for pl,un in pairs(mis.players) do
                            if Utils.isInAir(un) then
                                allLanded = false
                            else
                                allInAir = false
                            end
                        end

                        if allLanded then
                            mis:updateState(Mission.states.preping)
                            mis:pushMessageToPlayers(mis.name..' mission is in the prep phase')
                        end

                        if allInAir then
                            mis:updateState(Mission.states.active)
                            mis:pushMessageToPlayers(mis.name..' mission has started')
                            local missionstatus = mis:getDetailedDescription()
                            mis:pushMessageToPlayers(missionstatus)
                        end
                    elseif mis.state == Mission.states.active then
                        mis:updateObjectives()
                    elseif mis.state == Mission.states.completed then
                        local isInstant = mis:isInstantReward()
                        if isInstant then
                            mis:pushMessageToPlayers(mis.name..' mission complete.', 60)
                        else
                            mis:pushMessageToPlayers(mis.name..' mission complete. Land to claim rewards.', 60)
                        end 

                        for _,reward in ipairs(mis.rewards) do
                            for p,_ in pairs(mis.players) do
                                local finalAmount = reward.amount
                                if reward.type == PlayerTracker.statTypes.xp then
                                    finalAmount = math.floor(finalAmount * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(p))
                                end

                                if isInstant then
                                    DependencyManager.get("PlayerTracker"):addStat(p, finalAmount, reward.type)
                                    mis:pushMessageToPlayer(p, '+'..reward.amount..' '..reward.type)
                                else
                                    DependencyManager.get("PlayerTracker"):addTempStat(p, finalAmount, reward.type)
                                end
                            end
                        end

                        for p,u in pairs(mis.players) do
                            DependencyManager.get("PlayerTracker"):addRankRewards(p,u, not isInstant)
                        end

                        mis:pushSoundToPlayers("success.ogg")
                        param.activeMissions[code] = nil
                        env.info('Mission code'..code..' removed due to completion')
                    elseif mis.state == Mission.states.failed then
                        local msg = mis.name..' mission failed.'
                        if mis.failureReason then
                            msg = msg..'\n'..mis.failureReason
                        end

                        mis:pushMessageToPlayers(msg, 60)

                        mis:pushSoundToPlayers("fail.ogg")
                        param.activeMissions[code] = nil
                        env.info('Mission code'..code..' removed due to failure')
                    end
                end
            end

            return time+1
        end, self, timer.getTime()+1)

        local ev = {}
		ev.context = self
		function ev:onEvent(event)
			if event.id == world.event.S_EVENT_KILL and event.initiator and event.target and event.initiator.getPlayerName then
				local player = event.initiator:getPlayerName()
				if player and 
                    event.initiator:isExist() and
                    event.initiator.getCoalition and 
                    event.target.getCoalition and 
                    event.initiator:getCoalition() ~= event.target:getCoalition() then
					    self.context:tallyKill(player, event.target)
				end
			end

            if event.id == world.event.S_EVENT_SHOT and event.initiator and event.weapon and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
				if player and event.initiator:isExist() and event.weapon:isExist() then
                    self.context:tallyWeapon(player, event.weapon)
                end
            end
		end
		
		world.addEventHandler(ev)
	end

    function MissionTracker:generateMission(misType)
        local misid = self:getNewMissionID()
        env.info('MissionTracker - generating mission type ['..misType..'] id code['..misid..']')
        
        local newmis = nil
        if misType == Mission.types.cap_easy then
            newmis = CAP_Easy:new(misid, misType)
        elseif misType == Mission.types.cap_medium then
            newmis = CAP_Medium:new(misid, misType)
        elseif misType == Mission.types.cas_easy then
            newmis = CAS_Easy:new(misid, misType)
        elseif misType == Mission.types.cas_medium then
            newmis = CAS_Medium:new(misid, misType)
        elseif misType == Mission.types.cas_hard then
            newmis = CAS_Hard:new(misid, misType)
        elseif misType == Mission.types.sead then
            newmis = SEAD:new(misid, misType)
        elseif misType == Mission.types.supply_easy then
            newmis = Supply_Easy:new(misid, misType)
        elseif misType == Mission.types.supply_hard then
            newmis = Supply_Hard:new(misid, misType)
        elseif misType == Mission.types.strike_veryeasy then
            newmis = Strike_VeryEasy:new(misid, misType)
        elseif misType == Mission.types.strike_easy then
            newmis = Strike_Easy:new(misid, misType)
        elseif misType == Mission.types.strike_medium then
            newmis = Strike_Medium:new(misid, misType)
        elseif misType == Mission.types.strike_hard then
            newmis = Strike_Hard:new(misid, misType)
        elseif misType == Mission.types.deep_strike then
            newmis = Deep_Strike:new(misid, misType)
        elseif misType == Mission.types.dead then
            newmis = DEAD:new(misid, misType)
        elseif misType == Mission.types.escort then
            newmis = Escort:new(misid, misType)
        elseif misType == Mission.types.tarcap then
            newmis = TARCAP:new(misid, misType, self.activeMissions)
        elseif misType == Mission.types.recon then
            newmis = Recon:new(misid, misType)
        elseif misType == Mission.types.bai then
            newmis = BAI:new(misid, misType)
        elseif misType == Mission.types.anti_runway then
            newmis = Anti_Runway:new(misid, misType)
        elseif misType == Mission.types.csar then
            newmis = CSAR:new(misid, misType)
        elseif misType == Mission.types.extraction then
            newmis = Extraction:new(misid, misType)
        elseif misType == Mission.types.deploy_squad then
            newmis = DeploySquad:new(misid, misType)
        end

        if not newmis then return end

        if #newmis.objectives == 0 then return end

        self.missionBoard[misid] = newmis
        env.info('MissionTracker - generated mission id code'..misid..' \n'..newmis.description)
        trigger.action.outTextForCoalition(2,'New mission available: '..newmis.name,5)
    end

    function MissionTracker:tallyWeapon(player, weapon)
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    if Weapon.getCategoryEx(weapon) == Weapon.Category.BOMB then
                        timer.scheduleFunction(function (params, time)
                            if not params.weapon:isExist() then
                                return nil -- weapon despawned
                            end

                            local alt = Utils.getAGL(params.weapon)
                            if alt < 5 then 
                                params.mission:tallyWeapon(params.weapon)
                                return nil
                            end

                            if alt < 20 then
                                return time+0.01
                            end

                            return time+0.1
                        end, {player = player, weapon = weapon, mission = m}, timer.getTime()+0.1)
                    end
                end
            end
        end
    end

    function MissionTracker:tallyKill(player,kill)
        env.info("MissionTracker - tallyKill: "..player.." killed "..kill:getName())
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    m:tallyKill(kill)
                end
            end
        end
    end

    function MissionTracker:tallySupplies(player, amount, zonename)
        env.info("MissionTracker - tallySupplies: "..player.." delivered "..amount.." of supplies to "..zonename)
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    m:tallySupplies(amount, zonename)
                end
            end
        end
    end

    function MissionTracker:tallyLoadPilot(player, pilot)
        env.info("MissionTracker - tallyLoadPilot: "..player.." loaded pilot "..pilot.name)
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    m:tallyLoadPilot(player, pilot)
                end
            end
        end
    end

    function MissionTracker:tallyUnloadPilot(player, zonename)
        env.info("MissionTracker - tallyUnloadPilot: "..player.." unloaded pilots at "..zonename)
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    m:tallyUnloadPilot(player, zonename)
                end
            end
        end
    end

    function MissionTracker:tallyLoadSquad(player, squad)
        env.info("MissionTracker - tallyLoadSquad: "..player.." loaded squad "..squad.name)
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    m:tallyLoadSquad(player, squad)
                end
            end
        end
    end

    function MissionTracker:tallyUnloadSquad(player, zonename, squadType)
        env.info("MissionTracker - tallyUnloadSquad: "..player.." unloaded "..squadType.." squad at "..zonename)
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    m:tallyUnloadSquad(player, zonename, squadType)
                end
            end
        end
    end

    function MissionTracker:tallyRecon(player, targetzone, analyzezonename)
        env.info("MissionTracker - tallyRecon: "..player.." analyzed "..targetzone.." recon data at "..analyzezonename)
        for _,m in pairs(self.activeMissions) do
            if m.players[player] then
                if m.state == Mission.states.active then
                    m:tallyRecon(player, targetzone, analyzezonename)
                end
            end
        end
    end

    function MissionTracker:activateOrJoinMissionForGroup(code, groupname)
        if groupname then
            env.info('MissionTracker - activateOrJoinMissionForGroup: '..tostring(groupname)..' requested activate or join '..code)
            local gr = Group.getByName(groupname)
            for i,v in ipairs(gr:getUnits()) do
                if v.getPlayerName and v:getPlayerName() then 
                    local mis = self.activeMissions[code]
                    if mis then 
                        self:joinMission(code, v:getPlayerName(), v)
                    else
                        self:activateMission(code, v:getPlayerName(), v)
                    end
                    return
                end
            end
        end
    end

    function MissionTracker:activateMission(code, player, unit)
        if Config.restrictMissionAcceptance then
            if not unit or not unit:isExist() or not Utils.isLanded(unit, true) then 
                if unit and unit:isExist() then trigger.action.outTextForUnit(unit:getID(), 'Can only accept mission while landed', 5) end
                return false 
            end

            local zn = ZoneCommand.getZoneOfUnit(unit:getName())
            if not zn then 
                zn = CarrierCommand.getCarrierOfUnit(unit:getName())
            end

            if not zn or zn.side ~= unit:getCoalition() then 
                trigger.action.outTextForUnit(unit:getID(), 'Can only accept mission while inside friendly zone', 5)
                return false 
            end
        end

        for c,m in pairs(self.activeMissions) do
            if m:getPlayerUnit(player) then 
                trigger.action.outTextForUnit(unit:getID(), 'A mission is already active.', 5)
                return false 
            end
        end

        local mis = self.missionBoard[code]
        if not mis then 
            trigger.action.outTextForUnit(unit:getID(), 'Invalid mission code', 5)
            return false 
        end

        if mis.state ~= Mission.states.new then
            trigger.action.outTextForUnit(unit:getID(), 'Invalid mission.', 5)
            return false 
        end

        if not mis:isUnitTypeAllowed(unit) then
            trigger.action.outTextForUnit(unit:getID(), 'Current aircraft type is not compatible with this mission.', 5)
            return false
        end

        self.missionBoard[code] = nil
        
        trigger.action.outTextForCoalition(2,'Mission ['..mis.missionID..'] '..mis.name..' was accepted by '..player,5)
        mis:updateState(Mission.states.preping)
        mis.missionID = self:getNewMissionID()
        mis:addPlayer(player, unit)

        mis:pushMessageToPlayers(mis.name..' accepted.\nJoin code: ['..mis.missionID..']')

        env.info('Mission code'..code..' changed to code'..mis.missionID)
        env.info('Mission code'..mis.missionID..' accepted by '..player)
        self.activeMissions[mis.missionID] = mis
        return true
    end

    function MissionTracker:joinMission(code, player, unit)
        if Config.restrictMissionAcceptance then
            if not unit or not unit:isExist() or not Utils.isLanded(unit, true) then 
                if unit and unit:isExist() then trigger.action.outTextForUnit(unit:getID(), 'Can only join mission while landed', 5) end
                return false
            end

            local zn = ZoneCommand.getZoneOfUnit(unit:getName())
            if not zn then 
                zn = CarrierCommand.getCarrierOfUnit(unit:getName())
            end

            if not zn or zn.side ~= unit:getCoalition() then 
                trigger.action.outTextForUnit(unit:getID(), 'Can only join mission while inside friendly zone', 5)
                return false
            end
        end
       
        for c,m in pairs(self.activeMissions) do
            if m:getPlayerUnit(player) then 
                trigger.action.outTextForUnit(unit:getID(), 'A mission is already active.', 5)
                return false 
            end
        end

        local mis = self.activeMissions[code]
        if not mis then 
            trigger.action.outTextForUnit(unit:getID(), 'Invalid mission code', 5)
            return false 
        end

        if mis.state ~= Mission.states.preping then
            trigger.action.outTextForUnit(unit:getID(), 'Mission can only be joined if its members have not taken off yet.', 5)
            return false 
        end

        if not mis:isUnitTypeAllowed(unit) then
            trigger.action.outTextForUnit(unit:getID(), 'Current aircraft type is not compatible with this mission.', 5)
            return false
        end
        
        mis:addPlayer(player, unit)
        mis:pushMessageToPlayers(player..' has joined mission '..mis.name)
        env.info('Mission code'..code..' joined by '..player)
        return true
    end

    function MissionTracker:leaveMission(player)
        for _,mis in pairs(self.activeMissions) do
            if mis:getPlayerUnit(player) then
                mis:pushMessageToPlayers(player..' has left mission '..mis.name)
                mis:removePlayer(player)
                env.info('Mission code'..mis.missionID..' left by '..player)
                if not mis:hasPlayers() then
                    self.activeMissions[mis.missionID]:updateState(Mission.states.failed)
                    self.activeMissions[mis.missionID] = nil
                    env.info('Mission code'..mis.missionID..' canceled due to all players leaving')
                end

                break
            end
        end
        
        return true
    end

    function MissionTracker:canCreateMission(misType)
        if not MissionTracker.maxMissionCount[misType] then return false end
        
        local missionCount = 0
        for i,v in pairs(self.missionBoard) do
            if v.type == misType then missionCount = missionCount + 1 end
        end

        for i,v in pairs(self.activeMissions) do
            if v.type == misType then missionCount = missionCount + 1 end
        end

        if missionCount >= MissionTracker.maxMissionCount[misType] then return false end

        if misType == Mission.types.cap_easy then
            return CAP_Easy.canCreate()
        elseif misType == Mission.types.cap_medium then
            return CAP_Medium.canCreate()
        elseif misType == Mission.types.cas_easy then
            return CAS_Easy.canCreate()
        elseif misType == Mission.types.cas_medium then
            return CAS_Medium.canCreate()
        elseif misType == Mission.types.sead then
            return SEAD.canCreate()
        elseif misType == Mission.types.dead then
            return DEAD.canCreate()
        elseif misType == Mission.types.cas_hard then
            return CAS_Hard.canCreate()
        elseif misType == Mission.types.supply_easy then
            return Supply_Easy.canCreate()
        elseif misType == Mission.types.supply_hard then
            return Supply_Hard.canCreate()
        elseif misType == Mission.types.strike_veryeasy then
            return Strike_VeryEasy.canCreate()
        elseif misType == Mission.types.strike_easy then
            return Strike_Easy.canCreate()
        elseif misType == Mission.types.strike_medium then
            return Strike_Medium.canCreate()
        elseif misType == Mission.types.strike_hard then
            return Strike_Hard.canCreate()
        elseif misType == Mission.types.deep_strike then
            return Deep_Strike.canCreate()
        elseif misType == Mission.types.escort then
            return Escort.canCreate()
        elseif misType == Mission.types.tarcap then
            return TARCAP.canCreate(self.activeMissions)
        elseif misType == Mission.types.recon then
            return Recon.canCreate()
        elseif misType == Mission.types.bai then
            return BAI.canCreate()
        elseif misType == Mission.types.anti_runway then
            return Anti_Runway.canCreate()
        elseif misType == Mission.types.csar then
            return CSAR.canCreate()
        elseif misType == Mission.types.extraction then
            return Extraction.canCreate()
        elseif misType == Mission.types.deploy_squad then
            return DeploySquad.canCreate()
        end

        return false
    end

end


-----------------[[ END OF MissionTracker.lua ]]-----------------



-----------------[[ SquadTracker.lua ]]-----------------

SquadTracker = {}
do
	function SquadTracker:new()
		local obj = {}
		obj.activeInfantrySquads = {}
		setmetatable(obj, self)
		self.__index = self
		
		obj:start()
        
		DependencyManager.register("SquadTracker", obj)
		return obj
	end

    SquadTracker.infantryCallsigns = {
        adjectives = {"Sapphire", "Emerald", "Whisper", "Vortex", "Blaze", "Nova", "Silent", "Zephyr", "Radiant", "Shadow", "Lively", "Dynamo", "Dusk", "Rapid", "Stellar", "Tundra", "Obsidian", "Cascade", "Zenith", "Solar"},
        nouns = {"Journey", "Quasar", "Galaxy", "Moonbeam", "Comet", "Starling", "Serenade", "Raven", "Breeze", "Echo", "Avalanche", "Harmony", "Stardust", "Horizon", "Firefly", "Solstice", "Labyrinth", "Whisper", "Cosmos", "Mystique"}
    }

    function SquadTracker:generateCallsign()
        local adjective = self.infantryCallsigns.adjectives[math.random(1,#self.infantryCallsigns.adjectives)]
        local noun = self.infantryCallsigns.nouns[math.random(1,#self.infantryCallsigns.nouns)]

        local callsign = adjective..noun

        if self.activeInfantrySquads[callsign] then
            for i=1,1000,1 do
                local try = callsign..'-'..i
                if not self.activeInfantrySquads[try] then
                    callsign = try
                    break
                end
            end
        end

        if not self.activeInfantrySquads[callsign] then
            return callsign
        end
    end

    function SquadTracker:restoreInfantry(save)

        Spawner.createObject(save.name, save.data.name, save.position, save.side, 20, 30,{
            [land.SurfaceType.LAND] = true, 
            [land.SurfaceType.ROAD] = true,
            [land.SurfaceType.RUNWAY] = true,
        })

        self.activeInfantrySquads[save.name] = {
            name = save.name, 
            position = save.position, 
            state = save.state, 
            remainingStateTime=save.remainingStateTime, 
            shouldDiscover = save.discovered,
            discovered = save.discovered,
            data = save.data
        }

        if save.state == "extractReady" then
            MissionTargetRegistry.addSquad(self.activeInfantrySquads[save.name])
        end
        
        env.info('SquadTracker - '..save.name..'('..save.data.type..') restored')
    end

    function SquadTracker:spawnInfantry(infantryData, position)
        local callsign = self:generateCallsign()
        if callsign then
            Spawner.createObject(callsign, infantryData.name, position, infantryData.side, 20, 30,{
                [land.SurfaceType.LAND] = true, 
                [land.SurfaceType.ROAD] = true,
                [land.SurfaceType.RUNWAY] = true,
            })

            self:registerInfantry(infantryData, callsign, position)
        end
    end

    function SquadTracker:registerInfantry(infantryData, groupname, position)
        self.activeInfantrySquads[groupname] = {name = groupname, position = position, state = "deployed", remainingStateTime=0, data = infantryData}
        
        env.info('SquadTracker - '..groupname..'('..infantryData.type..') deployed')
    end
	
    function SquadTracker:start()
		if not ZoneCommand then return end

        timer.scheduleFunction(function(param, time)
			local self = param.context
			
			for i,v in pairs(self.activeInfantrySquads) do
                local remove = self:processInfantrySquad(v)
                if remove then
                    MissionTargetRegistry.removeSquad(v)
                    self.activeInfantrySquads[v.name] = nil
                end
			end
			
			return time+10
		end, {context = self}, timer.getTime()+1)
    end

    function SquadTracker:removeSquad(squadname)
        local squad = self.activeInfantrySquads[squadname]
        if squad then
            MissionTargetRegistry.removeSquad(squad)
            squad.state = 'extracted'
            squad.remainingStateTime = 0
            self.activeInfantrySquads[squadname] = nil
        end
    end

    function SquadTracker:getClosestExtractableSquad(sourcePoint, onside)
        local minDist = 99999999
        local squad = nil

        for i,v in pairs(self.activeInfantrySquads) do
            if v.state == 'extractReady' and v.data.side == onside then
                local gr = Group.getByName(v.name)
                if gr and gr:getSize()>0 then
                    local dist = mist.utils.get2DDist(sourcePoint, gr:getUnit(1):getPoint())
                    if dist<minDist then
                        minDist = dist
                        squad = v
                    end
                end
            end
        end

        if squad then
            return squad, minDist
        end
    end

    --[[
        infantry states:
            deployed - just spawned not processed yet
            working - started main activity, last until jobtime elapses
            extractReady - job completed waiting for extract, lasts until extracttime elapses
            mia - missing in action, extracttime elapsed without extraction, group is forever lost
            complete - mission complete no extraction necessary
            extracted - squad was loaded into a player helicopter
    ]] 
    function SquadTracker:processInfantrySquad(squad)
        local gr = Group.getByName(squad.name)
        if not gr then return true end
		if gr:getSize()==0 then 
			gr:destroy()
			return true
		end

        squad.remainingStateTime = squad.remainingStateTime - 10

        if squad.state == 'deployed' then
            env.info('SquadTracker - '..squad.name..'('..squad.data.type..') started working for '..squad.data.jobtime..' seconds')
            squad.state = 'working'
            squad.remainingStateTime = squad.data.jobtime
        elseif squad.state == 'working' then
            if squad.remainingStateTime <=0 then
                env.info('SquadTracker - '..squad.name..'('..squad.data.type..') job complete, waiting '..squad.data.extracttime..' seconds for extract')
                
                if squad.data.type == PlayerLogistics.infantryTypes.capture then
                    local zn = ZoneCommand.getZoneOfPoint(squad.position)
                    if zn and zn.side == 0 then
                        squad.state = "complete"
                        squad.remainingStateTime = 0
                        zn:capture(gr:getCoalition())
                        gr:destroy()
                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') no extraction required, deleting')
                        return true
                    else
                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') not in zone, cant capture')
                    end
                elseif squad.data.type == PlayerLogistics.infantryTypes.engineer then
                    local zn = ZoneCommand.getZoneOfPoint(squad.position)
                    if zn and zn.side == gr:getCoalition() then
                        zn:boostProduction(3000)
                        squad.state = "complete"
                        squad.remainingStateTime = 0
                        gr:destroy()
                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') no extraction required, deleting')
                        return true
                    else
                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') not in zone, cant boost')
                    end
                elseif squad.data.type == PlayerLogistics.infantryTypes.sabotage then
                    local zn = ZoneCommand.getZoneOfPoint(squad.position)
                    if zn and zn.side ~= gr:getCoalition() and zn.side ~= 0 then
                        zn:sabotage(1000, squad.position)
                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') sabotaged '..zn.name)
                    else
                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') not in zone, cant sabotage')
                    end
                elseif squad.data.type == PlayerLogistics.infantryTypes.spy then
                    local zn = ZoneCommand.getZoneOfPoint(squad.position)
                    if zn and zn.side ~= gr:getCoalition() and zn.side ~= 0 then
                        zn:reveal()

                        if zn.neighbours then
                            for _,v in pairs(zn.neighbours) do
                                if v.side ~= gr:getCoalition() and v.side ~= 0 then
                                    v:reveal()
                                    if v:hasUnitWithAttributeOnSide({'Buildings'}, v.side) then
                                        local tgt = v:getRandomUnitWithAttributeOnSide({'Buildings'}, v.side)
                                        if tgt then
                                            MissionTargetRegistry.addStrikeTarget(tgt, v)
                                        end
                                    end
                                end
                            end
                        end

                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') infiltrated into '..zn.name)
                    else
                        env.info('SquadTracker - '..squad.name..'('..squad.data.type..') not in zone, cant infiltrate')
                    end
                elseif squad.data.type == PlayerLogistics.infantryTypes.ambush then
                    local cnt = gr:getController()
                    cnt:setCommand({ 
                        id = 'SetInvisible', 
                        params = { 
                            value = false 
                        } 
                    })
                end

                squad.state = 'extractReady'
                squad.remainingStateTime = squad.data.extracttime
                MissionTargetRegistry.addSquad(squad)
			else
                if squad.data.type == PlayerLogistics.infantryTypes.ambush then
                    if not squad.discovered then
                        local frcnt = gr:getUnit(1):getController()
                        local targets = frcnt:getDetectedTargets()
                        local isTargetClose = false
                        if #targets > 0 then
                            for _,tgt in ipairs(targets) do
                                if tgt.visible and tgt.object then
                                    if tgt.object.isExist and tgt.object:isExist() and tgt.object.getCoalition and tgt.object:getCoalition()~=gr:getCoalition() and 
                                    Object.getCategory(tgt.object) == 1 then
                                        local dist = mist.utils.get3DDist(gr:getUnit(1):getPoint(), tgt.object:getPoint())
                                        if dist < 100 then
                                            isTargetClose = true
                                            break
                                        end
                                    end
                                end
                            end
                        end
                        
                        if isTargetClose then
                            squad.discovered = true
                            local cnt = gr:getController()
                            cnt:setCommand({ 
                                id = 'SetInvisible',
                                params = { 
                                    value = false 
                                }
                            })
                        end
                    elseif squad.shouldDiscover then
                        squad.shouldDiscover = nil
                        local cnt = gr:getController()
                        cnt:setCommand({ 
                            id = 'SetInvisible',
                            params = { 
                                value = false 
                            }
                        })
                    end
                end
            end
        elseif squad.state == 'extractReady' then
            if squad.remainingStateTime <= 0 then
                env.info('SquadTracker - '..squad.name..'('..squad.data.type..') extract time elapsed, group MIA')
                squad.state = 'mia'
                squad.remainingStateTime = 0
			    gr:destroy()
                MissionTargetRegistry.removeSquad(squad)
                return true
            end

            if not squad.lastMarkerDeployedTime then
                squad.lastMarkerDeployedTime = timer.getAbsTime() - (10*60)
            end

            if timer.getAbsTime() - squad.lastMarkerDeployedTime > (5*60) then
                if gr:getSize()>0 then
                    local unPos = gr:getUnit(1):getPoint()
                    local p = Utils.getPointOnSurface(unPos)
                    p.x = p.x + math.random(-5,5)
                    p.z = p.z + math.random(-5,5)
		            trigger.action.smoke(p, trigger.smokeColor.Blue)
                    squad.lastMarkerDeployedTime = timer.getAbsTime()
                end
            end
        end
    end
end

-----------------[[ END OF SquadTracker.lua ]]-----------------



-----------------[[ CSARTracker.lua ]]-----------------

CSARTracker = {}
do
	function CSARTracker:new()
		local obj = {}
		obj.activePilots = {}
		setmetatable(obj, self)
		self.__index = self
		
		obj:start()

		DependencyManager.register("CSARTracker", obj)
		return obj
	end

    function CSARTracker:start()
		if not ZoneCommand then return end

        local ev = {}
		ev.context = self
		function ev:onEvent(event)
			if event.id == world.event.S_EVENT_LANDING_AFTER_EJECTION then
                if event.initiator and event.initiator:isExist() then
                    if event.initiator:getCoalition() == 2 then
                        local z = ZoneCommand.getZoneOfPoint(event.initiator:getPoint())
                        if not z then
                            local name = self.context:generateCallsign()
                            if name then
                                local pos = {
                                    x = event.initiator:getPoint().x,
                                    y = event.initiator:getPoint().z
                                }

                                if pos.x ~= 0 and pos.y ~= 0 then
                                    local srfType = land.getSurfaceType(pos)
                                    if srfType ~= land.SurfaceType.WATER and srfType ~= land.SurfaceType.SHALLOW_WATER then
                                        local gr = Spawner.createPilot(name, pos)
                                        self.context:addPilot(name, gr)
                                    end
                                end
                            end
                        end
                    end

                    event.initiator:destroy()
                end
			end
		end
		
		world.addEventHandler(ev)

        timer.scheduleFunction(function(param, time)
            for i,v in pairs(param.context.activePilots) do
                v.remainingTime = v.remainingTime - 10
                if not v.pilot:isExist() or v.remainingTime <=0 then
                    param.context:removePilot(i)
                end
            end
			
			return time+10
		end, {context = self}, timer.getTime()+1)
    end

    function CSARTracker:markPilot(data)
        local pilot = data.pilot
        if pilot:isExist() then
            local pos = pilot:getUnit(1):getPoint()
            local p = Utils.getPointOnSurface(pos)
            p.x = p.x + math.random(-5,5)
            p.z = p.z + math.random(-5,5)
            trigger.action.smoke(p, trigger.smokeColor.Green)
        end
    end

    function CSARTracker:flarePilot(data)
        local pilot = data.pilot
        if pilot:isExist() then
            local pos = pilot:getUnit(1):getPoint()
            local p = Utils.getPointOnSurface(pos)
            trigger.action.signalFlare(p, trigger.flareColor.Green, math.random(1,360))
        end
    end

    function CSARTracker:removePilot(name)
        local data = self.activePilots[name]
        if data.pilot and data.pilot:isExist() then data.pilot:destroy() end

        MissionTargetRegistry.removePilot(data)
        self.activePilots[name] = nil
    end

    function CSARTracker:addPilot(name, pilot)
        self.activePilots[name] = {pilot = pilot, name = name, remainingTime = 45*60}
        MissionTargetRegistry.addPilot(self.activePilots[name])
    end

    function CSARTracker:restorePilot(save)         
        local gr = Spawner.createPilot(save.name, save.pos)
        
        self.activePilots[save.name] = {
            pilot = gr, 
            name = save.name, 
            remainingTime = save.remainingTime
        }
        
        MissionTargetRegistry.addPilot(self.activePilots[save.name])       
    end

    function CSARTracker:getClosestPilot(toPosition)
        local minDist = 99999999
        local data = nil
        local name = nil

        for i,v in pairs(self.activePilots) do
            if v.pilot:isExist() and v.pilot:getSize()>0 and v.pilot:getUnit(1):isExist() and v.remainingTime > 0 then
                local dist = mist.utils.get2DDist(toPosition, v.pilot:getUnit(1):getPoint())
                if dist<minDist then
                    minDist = dist
                    data = v
                end
            else
                self:removePilot(i)
            end
        end

        if data then
            return {name=data.name, pilot=data.pilot, remainingTime = data.remainingTime, dist=minDist}
        end
    end

    CSARTracker.pilotCallsigns = {
        adjectives = {"Agile", "Voyager", "Sleek", "Fierce", "Nimble", "Daring", "Swift", "Fearless", "Dynamic", "Rapid", "Brave", "Stealthy", "Eagle", "Thunder", "Roaring", "Jade", "Lion", "Crimson", "Mighty", "Phoenix"},
        nouns = {"Pancake", "Noodle", "Potato", "Banana", "Wombat", "Penguin", "Llama", "Cabbage", "Kangaroo", "Giraffe", "Walrus", "Pickle", "Donut", "Hamburger", "Toaster", "Teapot", "Unicorn", "Rainbow", "Dragon", "Sasquatch"}
    }

    function CSARTracker:generateCallsign()
        local adjective = self.pilotCallsigns.adjectives[math.random(1,#self.pilotCallsigns.adjectives)]
        local noun = self.pilotCallsigns.nouns[math.random(1,#self.pilotCallsigns.nouns)]

        local callsign = adjective..noun

        if self.activePilots[callsign] then
            for i=1,1000,1 do
                local try = callsign..'-'..i
                if not self.activePilots[try] then
                    callsign = try
                    break
                end
            end
        end

        if not self.activePilots[callsign] then
            return callsign
        end
    end
end

-----------------[[ END OF CSARTracker.lua ]]-----------------



-----------------[[ GCI.lua ]]-----------------

GCI = {}

do
    function GCI:new(side)
        local o = {}
        o.side = side
        o.tgtSide = 0
        if side == 1 then
            o.tgtSide = 2
        elseif side == 2 then
            o.tgtSide = 1
        end

        o.radars = {}
        o.players = {}
        o.radarTypes = {
            'SAM SR',
            'EWR',
            'AWACS'
        }

        o.groupMenus = {}

        setmetatable(o, self)
		self.__index = self

        o:start()

		DependencyManager.register("GCI", o)
		return o
    end

    function GCI:registerPlayer(name, unit, warningRadius, metric)
        if warningRadius > 0 then
            local msg = "Warning radius set to "..warningRadius
            if metric then
                msg=msg.."km" 
            else
                msg=msg.."nm"
            end
            
            local wRadius = 0
            if metric then
                wRadius = warningRadius * 1000
            else
                wRadius = warningRadius * 1852
            end
            
            self.players[name] = {
                unit = unit, 
                warningRadius = wRadius,
                metric = metric
            }
            
            trigger.action.outTextForUnit(unit:getID(), msg, 10)
            DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_warning_radius", warningRadius)
            DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_metric", metric)
        else
            self.players[name] = nil
            trigger.action.outTextForUnit(unit:getID(), "GCI Reports disabled", 10)
            DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_warning_radius", nil)
            DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_metric", nil)
        end
    end

    function GCI:start()
        MenuRegistry:register(4, function(event, context)
			if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then
				local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
                    local groupname = event.initiator:getGroup():getName()
                    local unit = event.initiator
					
                    if context.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
                        context.groupMenus[groupid] = nil
                    end

                    if not context.groupMenus[groupid] then
                        
                        local menu = missionCommands.addSubMenuForGroup(groupid, 'GCI')
                        local setWR = missionCommands.addSubMenuForGroup(groupid, 'Set Warning Radius', menu)
                        local kmMenu = missionCommands.addSubMenuForGroup(groupid, 'KM', setWR)
                        local nmMenu = missionCommands.addSubMenuForGroup(groupid, 'NM', setWR)

                        missionCommands.addCommandForGroup(groupid, '10 KM',  kmMenu, Utils.log(context.registerPlayer), context, player, unit, 10, true)
                        missionCommands.addCommandForGroup(groupid, '25 KM',  kmMenu, Utils.log(context.registerPlayer), context, player, unit, 25, true)
                        missionCommands.addCommandForGroup(groupid, '50 KM',  kmMenu, Utils.log(context.registerPlayer), context, player, unit, 50, true)
                        missionCommands.addCommandForGroup(groupid, '100 KM', kmMenu, Utils.log(context.registerPlayer), context, player, unit, 100, true)
                        missionCommands.addCommandForGroup(groupid, '150 KM', kmMenu, Utils.log(context.registerPlayer), context, player, unit, 150, true)

                        missionCommands.addCommandForGroup(groupid, '5 NM',  nmMenu, Utils.log(context.registerPlayer), context, player, unit, 5, false)
                        missionCommands.addCommandForGroup(groupid, '10 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 10, false)
                        missionCommands.addCommandForGroup(groupid, '25 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 25, false)
                        missionCommands.addCommandForGroup(groupid, '50 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 50, false)
                        missionCommands.addCommandForGroup(groupid, '80 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 80, false)
                        missionCommands.addCommandForGroup(groupid, 'Disable Warning Radius', menu, Utils.log(context.registerPlayer), context, player, unit, 0, false)

                        context.groupMenus[groupid] = menu
                    end
				end
            elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then
                local player = event.initiator:getPlayerName()
				if player then
					local groupid = event.initiator:getGroup():getID()
					
                    if context.groupMenus[groupid] then
                        missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid])
                        context.groupMenus[groupid] = nil
                    end
				end
            end
		end, self)

        timer.scheduleFunction(function(param, time)
            local self = param.context
            local allunits = coalition.getGroups(self.side)
  
            local radars = {}
            for _,g in ipairs(allunits) do
                for _,u in ipairs(g:getUnits()) do
                    for _,a in ipairs(self.radarTypes) do
                        if u:hasAttribute(a) then
                            table.insert(radars, u)
                            break
                        end
                    end
                end
            end

            self.radars = radars
            env.info("GCI - tracking "..#radars.." radar enabled units")

            return time+10
        end, {context = self}, timer.getTime()+1)

        timer.scheduleFunction(function(param, time)
            local self = param.context

            local plyCount = 0
            for i,v in pairs(self.players) do
                if not v.unit or not v.unit:isExist() then
                    self.players[i] = nil
                else
                    plyCount = plyCount + 1
                end
            end

            env.info("GCI - reporting to "..plyCount.." players")
            if plyCount >0 then
                local dect = {}
                local dcount = 0
                for _,u in ipairs(self.radars) do
                    if u:isExist() then
                        local detected = u:getController():getDetectedTargets(Controller.Detection.RADAR)
                        for _,d in ipairs(detected) do
                            if d and d.object and d.object.isExist and d.object:isExist() and 
                                Object.getCategory(d.object) == Object.Category.UNIT and
                                d.object.getCoalition and
                                d.object:getCoalition() == self.tgtSide then
                                    
                                if not dect[d.object:getName()] then
                                    dect[d.object:getName()] = d.object
                                    dcount = dcount + 1
                                end
                            end
                        end
                    end
                end
                
                env.info("GCI - aware of "..dcount.." enemy units")

                for name, data in pairs(self.players) do
                    if data.unit and data.unit:isExist() then
                        local closeUnits = {}

                        local wr = data.warningRadius
                        if wr > 0 then
                            for _,dt in pairs(dect) do
                                if dt:isExist() then
                                    local tgtPnt = dt:getPoint()
                                    local dist = mist.utils.get2DDist(data.unit:getPoint(), tgtPnt)
                                    if dist <= wr then
                                        local brg = math.floor(Utils.getBearing(data.unit:getPoint(), tgtPnt))

                                        local myPos = data.unit:getPosition()
                                        local tgtPos = dt:getPosition()
                                        local tgtHeading = math.deg(math.atan2(tgtPos.x.z, tgtPos.x.x))
                                        local tgtBearing = Utils.getBearing(tgtPos.p, myPos.p)
            
                                        local diff = math.abs(Utils.getHeadingDiff(tgtBearing, tgtHeading))
                                        local aspect = ''
                                        local priority = 1
                                        if diff <= 30 then
                                            aspect = "Hot"
                                            priority = 1
                                        elseif diff <= 60 then
                                            aspect = "Flanking"
                                            priority = 1
                                        elseif diff <= 120 then
                                            aspect = "Beaming"
                                            priority = 2
                                        else
                                            aspect = "Cold"
                                            priority = 3
                                        end

                                        table.insert(closeUnits, {
                                            type = dt:getDesc().typeName,
                                            bearing = brg,
                                            range = dist,
                                            altitude = tgtPnt.y,
                                            score = dist*priority,
                                            aspect = aspect
                                        })
                                    end
                                end
                            end
                        end

                        env.info("GCI - "..#closeUnits.." enemy units within "..wr.."m of "..name)
                        if #closeUnits > 0 then
                            table.sort(closeUnits, function(a, b) return a.range < b.range end)

                            local msg = "GCI Report:\n"
                            local count = 0
                            for _,tgt in ipairs(closeUnits) do
                                if data.metric then
                                    local km = tgt.range/1000
                                    if km < 1 then
                                        msg = msg..'\n'..tgt.type..'  MERGED'
                                    else
                                        msg = msg..'\n'..tgt.type..'  BRA: '..tgt.bearing..' for '
                                        msg = msg..Utils.round(km)..'km at '
                                        msg = msg..(Utils.round(tgt.altitude/250)*250)..'m, '
                                        msg = msg..tostring(tgt.aspect)
                                    end
                                else
                                    local nm = tgt.range/1852
                                    if nm < 1 then
                                        msg = msg..'\n'..tgt.type..'  MERGED'
                                    else
                                        msg = msg..'\n'..tgt.type..'  BRA: '..tgt.bearing..' for '
                                        msg = msg..Utils.round(nm)..'nm at '
                                        msg = msg..(Utils.round((tgt.altitude/0.3048)/1000)*1000)..'ft, '
                                        msg = msg..tostring(tgt.aspect)
                                    end
                                end
                                
                                count = count + 1
                                if count >= 10 then break end
                            end

                            trigger.action.outTextForUnit(data.unit:getID(), msg, 19)
                        end
                    else
                        self.players[name] = nil
                    end
                end
            end

            return time+20
        end, {context = self}, timer.getTime()+6)
    end
end

-----------------[[ END OF GCI.lua ]]-----------------



-----------------[[ Starter.lua ]]-----------------

Starter = {}
do
    Starter.neutralChance = 0.1

    function Starter.start(zones)
        if Starter.shouldRandomize() then
            Starter.randomize(zones)
        else
            Starter.normalStart(zones)
        end
    end

    function Starter.randomize(zones)
        local startZones = {}
        for _,z in pairs(zones) do
            if z.isHeloSpawn and z.isPlaneSpawn then
                table.insert(startZones, z)
            end
        end

        if #startZones > 0 then
            local sz = startZones[math.random(1,#startZones)]

            sz:capture(2, true)
            Starter.captureNeighbours(sz, math.random(1,3))
        end

        for _,z in pairs(zones) do
            if z.side == 0 then 
                if math.random() > Starter.neutralChance then
                    z:capture(1,true)
                end
            end

            if z.side ~= 0 then
                z:fullUpgrade(math.random(1,30)/100)
            end
        end
    end

    function Starter.captureNeighbours(zone, stepsLeft)
        if stepsLeft > 0 then
            for _,v in pairs(zone.neighbours) do
                if v.side == 0 then
                    if math.random() > Starter.neutralChance then
                        v:capture(2,true)
                    end
                    Starter.captureNeighbours(v, stepsLeft-1)
                end
            end
        end
    end

    function Starter.shouldRandomize()
        if lfs then
            local filename = lfs.writedir()..'Missions/Saves/randomize.lua'
            if lfs.attributes(filename) then
                return true
            end
		end
    end

    function Starter.normalStart(zones)
        for _,z in pairs(zones) do
            local i = z.initialState
            if i then
                if i.side and i.side ~= 0 then
                    z:capture(i.side, true)
                    z:fullUpgrade()
                    z:boostProduction(math.random(1,200))
                end
            end
        end
    end
end

-----------------[[ END OF Starter.lua ]]-----------------

