--- **Ops** - Combat Troops & Logistics Department.
--
-- ===
-- 
-- **CTLD** - MOOSE based Helicopter CTLD Operations.
-- 
-- ===
-- 
-- ## Missions:
--
-- ### [CTLD - Combat Troop & Logistics Deployment](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/CTLD)
-- 
-- ===
-- 
-- **Main Features:**
--
--    * MOOSE-based Helicopter CTLD Operations for Players.
--
-- ===
--
-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), bbirchnz (additional code!!)
-- ### Repack addition for crates: **Raiden**
-- ### Additional cool features: **Lekaa**
-- 
-- @module Ops.CTLD
-- @image OPS_CTLD.jpg

-- Last Update Oct 2025


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO CTLD_CARGO
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

do 

------------------------------------------------------
--- **CTLD_CARGO** class, extends Core.Base#BASE
-- @type CTLD_CARGO
-- @field #string ClassName Class name.
-- @field #number ID ID of this cargo.
-- @field #string Name Name for menu.
-- @field #table Templates Table of #POSITIONABLE objects.
-- @field #string CargoType Enumerator of Type.
-- @field #boolean HasBeenMoved Flag for moving.
-- @field #boolean LoadDirectly Flag for direct loading.
-- @field #number CratesNeeded Crates needed to build.
-- @field Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission.
-- @field #boolean HasBeenDropped True if dropped from heli.
-- @field #number PerCrateMass Mass in kg.
-- @field #number Stock Number of builds available, -1 for unlimited.
-- @field #string Subcategory Sub-category name.
-- @field #boolean DontShowInMenu Show this item in menu or not.
-- @field Core.Zone#ZONE Location Location (if set) where to get this cargo item.
-- @field #table ResourceMap Resource Map information table if it has been set for static cargo items.
-- @field #string StaticShape Individual shape if set.
-- @field #string StaticType Individual type if set.
-- @field #string StaticCategory Individual static category if set.
-- @field #list<#string> TypeNames Table of unit types able to pick this cargo up.
-- @field #number Stock0 Initial stock, if any given.
-- @extends Core.Base#BASE

---
-- @field #CTLD_CARGO CTLD_CARGO
CTLD_CARGO = {
  ClassName = "CTLD_CARGO",
  ID = 0,
  Name = "none",
  Templates = {},
  CargoType = "none",
  HasBeenMoved = false,
  LoadDirectly = false,
  CratesNeeded = 0,
  Positionable = nil,
  HasBeenDropped = false,
  PerCrateMass = 0,
  Stock = nil,
  Stock0 = nil,
  Mark = nil,
  DontShowInMenu = false,
  Location = nil,
  }
  
  --- Define cargo types.
  -- @type CTLD_CARGO.Enum
  -- @field #string VEHICLE
  -- @field #string TROOPS
  -- @field #string FOB
  -- @field #string CRATE
  -- @field #string REPAIR
  -- @field #string ENGINEERS
  -- @field #string STATIC
  -- @field #string GCLOADABLE
  CTLD_CARGO.Enum = {
    VEHICLE = "Vehicle", -- #string vehicles
    TROOPS = "Troops", -- #string troops
    FOB = "FOB", -- #string FOB
    CRATE = "Crate", -- #string crate
    REPAIR = "Repair", -- #string repair
    ENGINEERS = "Engineers", -- #string engineers
    STATIC = "Static", -- #string statics
    GCLOADABLE = "GC_Loadable", -- #string dynamiccargo
  }
  
  --- Function to create new CTLD_CARGO object.
  -- @param #CTLD_CARGO self
  -- @param #number ID ID of this #CTLD_CARGO
  -- @param #string Name Name for menu.
  -- @param #table Templates Table of #POSITIONABLE objects.
  -- @param #CTLD_CARGO.Enum Sorte Enumerator of Type.
  -- @param #boolean HasBeenMoved Flag for moving.
  -- @param #boolean LoadDirectly Flag for direct loading.
  -- @param #number CratesNeeded Crates needed to build.
  -- @param Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission.
  -- @param #boolean Dropped Cargo/Troops have been unloaded from a chopper.
  -- @param #number PerCrateMass Mass in kg
  -- @param #number Stock Number of builds available, nil for unlimited
  -- @param #string Subcategory Name of subcategory, handy if using > 10 types to load.
  -- @param #boolean DontShowInMenu Show this item in menu or not (default: false == show it).
  -- @param Core.Zone#ZONE Location (optional) Where the cargo is available (one location only).
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory, DontShowInMenu, Location)
    -- Inherit everything from BASE class.
    local self=BASE:Inherit(self, BASE:New()) -- #CTLD_CARGO
    self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped})
    self.ID = ID or math.random(100000,1000000)
    self.Name = Name or "none" -- #string
    self.Templates = Templates or {} -- #table
    self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum
    self.HasBeenMoved = HasBeenMoved or false -- #boolean
    self.LoadDirectly = LoadDirectly or false -- #boolean
    self.CratesNeeded = CratesNeeded or 0 -- #number
    self.Positionable = Positionable or nil -- Wrapper.Positionable#POSITIONABLE
    self.HasBeenDropped = Dropped or false --#boolean
    self.PerCrateMass = PerCrateMass or 0 -- #number
    self.Stock = Stock or nil --#number
    self.Stock0 = Stock or nil --#number 
    self.Mark = nil
    self.Subcategory = Subcategory or "Other"
    self.DontShowInMenu = DontShowInMenu or false
    self.ResourceMap = nil
    self.StaticType = "container_cargo" -- "container_cargo"
    self.StaticShape = nil
    self.TypeNames = nil
    self.StaticCategory = "Cargos"
    if type(Location) == "string" then
      Location = ZONE:New(Location)
    end
    self.Location = Location
    return self
  end
  
  --- Add specific static type and shape to this CARGO.
  -- @param #CTLD_CARGO self
  -- @param #string TypeName
  -- @param #string ShapeName
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:SetStaticTypeAndShape(Category,TypeName,ShapeName)
    self.StaticCategory = Category or "Cargos"
    self.StaticType = TypeName or "container_cargo"
    self.StaticShape = ShapeName
    return self
  end
  
  --- Get the specific static type and shape from this CARGO if set.
  -- @param #CTLD_CARGO self
  -- @return #string Category
  -- @return #string TypeName
  -- @return #string ShapeName
  function CTLD_CARGO:GetStaticTypeAndShape()
    return self.StaticCategory, self.StaticType, self.StaticShape
  end
  
  --- Add specific unit types to this CARGO (restrict what types can pick this up).
  -- @param #CTLD_CARGO self
  -- @param #string UnitTypes Unit type name, can also be a #list<#string> table of unit type names.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:AddUnitTypeName(UnitTypes)
    if not self.TypeNames then self.TypeNames = {} end
    if type(UnitTypes) ~= "table" then UnitTypes = {UnitTypes} end
    for _,_singletype in pairs(UnitTypes or {}) do
      self.TypeNames[_singletype]=_singletype
    end
    return self
  end
  
  --- Check if a specific unit can carry this CARGO (restrict what types can pick this up).
  -- @param #CTLD_CARGO self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD_CARGO:UnitCanCarry(Unit)
    if self.TypeNames == nil then return true end
    local typename = Unit:GetTypeName() or "none"
    if self.TypeNames[typename] then
      return true
    else
      return false
    end
  end
  
  --- Add Resource Map information table
  -- @param #CTLD_CARGO self
  -- @param #table ResourceMap
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:SetStaticResourceMap(ResourceMap)
    self.ResourceMap = ResourceMap
    return self
  end
  
  --- Get Resource Map information table
  -- @param #CTLD_CARGO self
  -- @return #table ResourceMap
  function CTLD_CARGO:GetStaticResourceMap()
    return self.ResourceMap
  end
  
  --- Query Location.
  -- @param #CTLD_CARGO self
  -- @return Core.Zone#ZONE location or `nil` if not set
  function CTLD_CARGO:GetLocation()
    return self.Location
  end
  
  --- Query ID.
  -- @param #CTLD_CARGO self
  -- @return #number ID
  function CTLD_CARGO:GetID()
    return self.ID
  end
  
  --- Query Subcategory
  -- @param #CTLD_CARGO self
  -- @return #string SubCategory
  function CTLD_CARGO:GetSubCat()
    return self.Subcategory
  end
  
  --- Query Mass.
  -- @param #CTLD_CARGO self
  -- @return #number Mass in kg
  function CTLD_CARGO:GetMass()
    return self.PerCrateMass
  end  
  
  --- Query Name.
  -- @param #CTLD_CARGO self
  -- @return #string Name
  function CTLD_CARGO:GetName()
    return self.Name
  end
  
  --- Query Templates.
  -- @param #CTLD_CARGO self
  -- @return #table Templates
  function CTLD_CARGO:GetTemplates()
    return self.Templates
  end
  
  --- Query has moved.
  -- @param #CTLD_CARGO self
  -- @return #boolean Has moved
  function CTLD_CARGO:HasMoved()
    return self.HasBeenMoved
  end
  
  --- Query was dropped.
  -- @param #CTLD_CARGO self
  -- @return #boolean Has been dropped.
  function CTLD_CARGO:WasDropped()
    return self.HasBeenDropped
  end
  
  --- Query directly loadable.
  -- @param #CTLD_CARGO self
  -- @return #boolean loadable
  function CTLD_CARGO:CanLoadDirectly()
    return self.LoadDirectly
  end
  
  --- Query number of crates or troopsize.
  -- @param #CTLD_CARGO self
  -- @return #number Crates or size of troops.
  function CTLD_CARGO:GetCratesNeeded()
    return self.CratesNeeded
  end
  
  --- Query type.
  -- @param #CTLD_CARGO self
  -- @return #CTLD_CARGO.Enum Type
  function CTLD_CARGO:GetType()
    return self.CargoType
  end
  
  --- Query type.
  -- @param #CTLD_CARGO self
  -- @return Wrapper.Positionable#POSITIONABLE Positionable
  function CTLD_CARGO:GetPositionable()
    return self.Positionable
  end
  
  --- Set HasMoved.
  -- @param #CTLD_CARGO self
  -- @param #boolean moved
  function CTLD_CARGO:SetHasMoved(moved)
    self.HasBeenMoved = moved or false
  end
  
   --- Query if cargo has been loaded.
  -- @param #CTLD_CARGO self
  -- @param #boolean loaded
  function CTLD_CARGO:Isloaded()
    if self.HasBeenMoved and not self:WasDropped() then
      return true
    else
     return false
    end 
  end
  
  --- Set WasDropped.
  -- @param #CTLD_CARGO self
  -- @param #boolean dropped
  function CTLD_CARGO:SetWasDropped(dropped)
    self.HasBeenDropped = dropped or false
  end
  
  --- Get Stock.
  -- @param #CTLD_CARGO self
  -- @return #number Stock or -1 if unlimited.
  function CTLD_CARGO:GetStock()
    if self.Stock then
      return self.Stock
    else
      return -1
    end
  end
  
  --- Get Stock0.
  -- @param #CTLD_CARGO self
  -- @return #number Stock0 or -1 if unlimited.
  function CTLD_CARGO:GetStock0()
    if self.Stock0 then
      return self.Stock0
    else
      return -1
    end
  end
  
    --- Get relative Stock.
  -- @param #CTLD_CARGO self
  -- @return #number Stock Percentage like 75, or -1 if unlimited.
  function CTLD_CARGO:GetRelativeStock()
    if self.Stock and self.Stock0 then
      return math.floor((self.Stock/self.Stock0)*100)
    else
      return -1
    end
  end
  
  --- Add Stock.
  -- @param #CTLD_CARGO self
  -- @param #number Number to add, none if nil.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:AddStock(Number)
    if self.Stock then -- Stock nil?
      local number = Number or 1
      self.Stock = self.Stock + number
    end
    return self
  end
  
  --- Remove Stock.
  -- @param #CTLD_CARGO self
  -- @param #number Number to reduce, none if nil.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:RemoveStock(Number)
    if self.Stock then -- Stock nil?
      local number = Number or 1
      self.Stock = self.Stock - number
      if self.Stock < 0 then self.Stock = 0 end
    end
    return self
  end
  
  --- Set Stock.
  -- @param #CTLD_CARGO self
  -- @param #number Number to set, nil means unlimited.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:SetStock(Number)
    self.Stock = Number
    return self
  end
  
  --- Query crate type for REPAIR
  -- @param #CTLD_CARGO self
  -- @param #boolean 
  function CTLD_CARGO:IsRepair()
   if self.CargoType == "Repair" then
    return true
   else
    return false
   end
  end
  
  --- Query crate type for STATIC
  -- @param #CTLD_CARGO self
  -- @return #boolean 
  function CTLD_CARGO:IsStatic()
   if self.CargoType == "Static" then
    return true
   else
    return false
   end
  end
  
  --- Add mark
  -- @param #CTLD_CARGO self
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:AddMark(Mark)
    self.Mark = Mark
    return self
  end
  
  --- Get mark
  -- @param #CTLD_CARGO self
  -- @return #string Mark
  function CTLD_CARGO:GetMark(Mark)
    return self.Mark
  end
  
  --- Wipe mark
  -- @param #CTLD_CARGO self
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:WipeMark()
    self.Mark = nil
    return self
  end
  
  --- Get overall mass of a cargo object, i.e. crates needed x mass per crate
  -- @param #CTLD_CARGO self
  -- @return #number mass
  function CTLD_CARGO:GetNetMass()
    return self.CratesNeeded * self.PerCrateMass
  end
   
end

do

------------------------------------------------------
--- **CTLD_ENGINEERING** class, extends Core.Base#BASE
-- @type CTLD_ENGINEERING
-- @field #string ClassName
-- @field #string lid
-- @field #string Name
-- @field Wrapper.Group#GROUP Group
-- @field Wrapper.Unit#UNIT Unit
-- @field Wrapper.Group#GROUP HeliGroup
-- @field Wrapper.Unit#UNIT HeliUnit
-- @field #string State
-- @extends Core.Base#BASE

---
-- @field #CTLD_ENGINEERING CTLD_ENGINEERING
CTLD_ENGINEERING = {
  ClassName = "CTLD_ENGINEERING",
  lid = "",
  Name = "none",
  Group = nil,
  Unit = nil,
  --C_Ops = nil,
  HeliGroup = nil,
  HeliUnit = nil,
  State = "",
  }
  
  --- CTLD_ENGINEERING class version.
  -- @field #string version
  CTLD_ENGINEERING.Version = "0.0.3"
  
  --- Create a new instance.
  -- @param #CTLD_ENGINEERING self
  -- @param #string Name
  -- @param #string GroupName Name of Engineering #GROUP object
  -- @param Wrapper.Group#GROUP HeliGroup HeliGroup
  -- @param Wrapper.Unit#UNIT HeliUnit HeliUnit
  -- @return #CTLD_ENGINEERING self 
  function CTLD_ENGINEERING:New(Name, GroupName, HeliGroup, HeliUnit)
  
      -- Inherit everything from BASE class.
    local self=BASE:Inherit(self, BASE:New()) -- #CTLD_ENGINEERING
    
   --BASE:I({Name, GroupName})
    
    self.Name = Name or "Engineer Squad" -- #string
    self.Group = GROUP:FindByName(GroupName) -- Wrapper.Group#GROUP
    self.Unit = self.Group:GetUnit(1) -- Wrapper.Unit#UNIT
    self.HeliGroup = HeliGroup -- Wrapper.Group#GROUP
    self.HeliUnit = HeliUnit -- Wrapper.Unit#UNIT
    self.currwpt = nil -- Core.Point#COORDINATE
    self.lid = string.format("%s (%s) | ",self.Name, self.Version)
      -- Start State.
    self.State = "Stopped"
    self.marktimer = 300 -- wait this many secs before trying a crate again
    self:Start()
    local parent = self:GetParent(self)
    return self
  end
  
  --- (Internal) Set the status
  -- @param #CTLD_ENGINEERING self
  -- @param #string State
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:SetStatus(State)
    self.State = State
    return self
  end
  
  --- (Internal) Get the status
  -- @param #CTLD_ENGINEERING self
  -- @return #string State
  function CTLD_ENGINEERING:GetStatus()
    return self.State
  end
  
  --- (Internal) Check the status
  -- @param #CTLD_ENGINEERING self
  -- @param #string State
  -- @return #boolean Outcome
  function CTLD_ENGINEERING:IsStatus(State)
    return self.State == State
  end
  
  --- (Internal) Check the negative status
  -- @param #CTLD_ENGINEERING self
  -- @param #string State
  -- @return #boolean Outcome
  function CTLD_ENGINEERING:IsNotStatus(State)
    return self.State ~= State
  end
  
  --- (Internal) Set start status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Start()
    self:T(self.lid.."Start")
    self:SetStatus("Running")
    return self
  end
  
  --- (Internal) Set stop status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Stop()
    self:T(self.lid.."Stop")
    self:SetStatus("Stopped")
    return self
  end
  
  --- (Internal) Set build status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Build()
    self:T(self.lid.."Build")
    self:SetStatus("Building")
    return self
  end
  
  --- (Internal) Set done status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Done()
    self:T(self.lid.."Done")
    local grp = self.Group -- Wrapper.Group#GROUP
    grp:RelocateGroundRandomInRadius(7,100,false,false,"Diamond")
    self:SetStatus("Running")
    return self
  end
  
  --- (Internal) Search for crates in reach.
  -- @param #CTLD_ENGINEERING self
  -- @param #table crates Table of found crate Ops.CTLD#CTLD_CARGO objects.
  -- @param #number number Number of crates found.
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Search(crates,number)
    self:T(self.lid.."Search")
    self:SetStatus("Searching")
    -- find crates close by
    --local COps = self.C_Ops -- Ops.CTLD#CTLD
    local dist = self.distance -- #number
    local group = self.Group -- Wrapper.Group#GROUP
    --local crates,number = COps:_FindCratesNearby(group,nil, dist) -- #table
    local ctable = {}
    local ind = 0
    if number > 0 then
      -- get set of dropped only
      for _,_cargo in pairs (crates) do
       local cgotype = _cargo:GetType()
       if _cargo:WasDropped() and cgotype ~= CTLD_CARGO.Enum.STATIC then
        local ok = false
        local chalk = _cargo:GetMark()
        if chalk == nil then
          ok = true
        else
         -- have we tried this cargo recently?
         local tag = chalk.tag or "none"
         local timestamp = chalk.timestamp or 0
         -- enough time gone?
         local gone = timer.getAbsTime() - timestamp
         if gone >= self.marktimer then
            ok = true
            _cargo:WipeMark()
         end -- end time check
        end -- end chalk
        if ok then
          local chalk = {}
          chalk.tag = "Engineers"
          chalk.timestamp = timer.getAbsTime()
          _cargo:AddMark(chalk)
          ind = ind + 1
          table.insert(ctable,ind,_cargo)
        end     
       end -- end dropped
      end -- end for
    end -- end number
    
    if ind > 0 then
      local crate = ctable[1] -- Ops.CTLD#CTLD_CARGO
      local static = crate:GetPositionable() -- Wrapper.Static#STATIC
      local crate_pos = static:GetCoordinate() -- Core.Point#COORDINATE
      local gpos = group:GetCoordinate() -- Core.Point#COORDINATE
      -- see how far we are from the crate
      local distance = self:_GetDistance(gpos,crate_pos)
      self:T(string.format("%s Distance to crate: %d", self.lid, distance))
      -- move there
      if distance > 30 and distance ~= -1 and self:IsStatus("Searching") then
        group:RouteGroundTo(crate_pos,15,"Line abreast",1)
        self.currwpt = crate_pos -- Core.Point#COORDINATE
        self:Move()
      elseif distance <= 30 and distance ~= -1 then
        -- arrived
        self:Arrive()
      end
    else
      self:T(self.lid.."No crates in reach!")
    end
    return self
  end
  
  --- (Internal) Move towards crates in reach.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Move()
    self:T(self.lid.."Move")
    self:SetStatus("Moving")
    -- check if we arrived on target
    --local COps = self.C_Ops -- Ops.CTLD#CTLD
    local group = self.Group -- Wrapper.Group#GROUP
    local tgtpos = self.currwpt -- Core.Point#COORDINATE
    local gpos = group:GetCoordinate() -- Core.Point#COORDINATE
    -- see how far we are from the crate
    local distance = self:_GetDistance(gpos,tgtpos)
    self:T(string.format("%s Distance remaining: %d", self.lid, distance))
    if distance <= 30 and distance ~= -1 then
        -- arrived
        self:Arrive()
    end
    return self
  end
  
  --- (Internal) Arrived at crates in reach. Stop group.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Arrive()
    self:T(self.lid.."Arrive")
    self:SetStatus("Arrived")
    self.currwpt = nil
    local Grp = self.Group -- Wrapper.Group#GROUP
    Grp:RouteStop()
    return self
  end
  
  --- (Internal) Return distance in meters between two coordinates.
  -- @param #CTLD_ENGINEERING self
  -- @param Core.Point#COORDINATE _point1 Coordinate one
  -- @param Core.Point#COORDINATE _point2 Coordinate two
  -- @return #number Distance in meters or -1
  function CTLD_ENGINEERING:_GetDistance(_point1, _point2)
    self:T(self.lid .. " _GetDistance")
    if _point1 and _point2 then
      local distance1 = _point1:Get2DDistance(_point2)
      local distance2 = _point1:DistanceFromPointVec2(_point2)
      if distance1 and type(distance1) == "number" then
        return distance1
      elseif distance2 and type(distance2) == "number" then
        return distance2
      else
        self:E("*****Cannot calculate distance!")
        self:E({_point1,_point2})
        return -1
      end
    else
      self:E("******Cannot calculate distance!")
      self:E({_point1,_point2})
      return -1
    end
  end

end

do
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO CTLD
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------
--- **CTLD** class, extends Core.Base#BASE, Core.Fsm#FSM
-- @type CTLD
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`.
-- @field #boolean debug
-- @extends Core.Fsm#FSM

--- *Combat Troop & Logistics Deployment (CTLD): Everyone wants to be a POG, until there\'s POG stuff to be done.* (Mil Saying)
--
-- ===
--
-- ![Banner Image](../Images/OPS_CTLD.jpg)
--
-- # CTLD Concept
-- 
--  * MOOSE-based CTLD for Players.
--  * Object oriented refactoring of Ciribob\'s fantastic CTLD script.
--  * No need for extra MIST loading. 
--  * Additional events to tailor your mission.
--  * ANY late activated group can serve as cargo, either as troops, crates, which have to be build on-location, or static like ammo chests.
--  * Option to persist (save&load) your dropped troops, crates and vehicles.
--  * Weight checks on loaded cargo.
-- 
-- ## 0. Prerequisites
-- 
-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that.
-- Create the late-activated troops, vehicles, that will make up your deployable forces.
-- 
-- Example sound files are here: [Moose Sound](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/CTLD%20CSAR)
-- 
-- ## 1. Basic Setup
-- 
-- ## 1.1 Create and start a CTLD instance
-- 
-- A basic setup example is the following:
--        
--        -- Instantiate and start a CTLD for the blue side, using helicopter groups named "Helicargo" and alias "Lufttransportbrigade I"
--        local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo"},"Lufttransportbrigade I")
--        my_ctld:__Start(5)
--
-- ## 1.2 Add cargo types available
--        
-- Add *generic* cargo types that you need for your missions, here infantry units, vehicles and a FOB. These need to be late-activated Wrapper.Group#GROUP objects:
--        
--        -- add infantry unit called "Anti-Tank Small" using template "ATS", of type TROOP with size 3
--        -- infantry units will be loaded directly from LOAD zones into the heli (matching number of free seats needed)
--        my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3)
--        -- if you want to add weight to your Heli, troops can have a weight in kg **per person**. Currently no max weight checked. Fly carefully.
--        my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3,80)
--        
--        -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4. No weight. We only have 2 in stock:
--        my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4,nil,2)
--        
--        -- add an engineers unit called "Wrenches" using template "Engineers", of type ENGINEERS with size 2. Engineers can be loaded, dropped,
--        -- and extracted like troops. However, they will seek to build and/or repair crates found in a given radius. Handy if you can\'t stay
--        -- to build or repair or under fire.
--        my_ctld:AddTroopsCargo("Wrenches",{"Engineers"},CTLD_CARGO.Enum.ENGINEERS,4)
--        myctld.EngineerSearch = 2000 -- teams will search for crates in this radius.
--        
--        -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build
--        -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2)
--        -- if you want to add weight to your Heli, crates can have a weight in kg **per crate**. Fly carefully.
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775)
--        -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock.
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10)
--        -- additionally, you can limit **where** the stock is available (one location only!) - this one is available in a zone called "Vehicle Store".
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10,nil,nil,"Vehicle Store")
--        
--        -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build:
--        my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4)
--        
--        -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair,
--        -- e.g. the "Humvee" here refers back to the "Humvee" crates cargo added above (same template!)
--        my_ctld:AddCratesRepair("Humvee Repair","Humvee",CTLD_CARGO.Enum.REPAIR,1)
--        my_ctld.repairtime = 300 -- takes 300 seconds to repair something
-- 
--        -- add static cargo objects, e.g ammo chests - the name needs to refer to a STATIC object in the mission editor, 
--        -- here: it\'s the UNIT name (not the GROUP name!), the second parameter is the weight in kg.
--        my_ctld:AddStaticsCargo("Ammunition",500)
--        
-- ## 1.3 Add logistics zones
--  
--  Add (normal, round!)  zones for loading troops and crates and dropping, building crates
--  
--        -- Add a zone of type LOAD to our setup. Players can load any troops and crates here as defined in 1.2 above.
--        -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside the zone.
--        -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon.
--        my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true)
--        
--        -- Add a zone of type DROP. Players can drop crates here.
--        -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon.
--        -- NOTE: Troops can be unloaded anywhere, also when hovering in parameters.
--        my_ctld:AddCTLDZone("Dropzone",CTLD.CargoZoneType.DROP,SMOKECOLOR.Red,true,true)
--        
--        -- Add two zones of type MOVE. Dropped troops and vehicles will move to the nearest one. See options.
--        -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon.
--        my_ctld:AddCTLDZone("Movezone",CTLD.CargoZoneType.MOVE,SMOKECOLOR.Orange,false,false)
--        
--        my_ctld:AddCTLDZone("Movezone2",CTLD.CargoZoneType.MOVE,SMOKECOLOR.White,true,true)
--        
--        -- Add a zone of type SHIP to our setup. Players can load troops and crates from this ship
--        -- "Tarawa" is the unitname (callsign) of the ship from the ME. Players can load, if they are inside the zone.
--        -- The ship is 240 meters long and 20 meters wide.
--        -- Note that you need to adjust the max hover height to deck height plus 5 meters or so for loading to work.
--        -- When the ship is moving, avoid forcing hoverload.
--        my_ctld:AddCTLDZone("Tarawa",CTLD.CargoZoneType.SHIP,SMOKECOLOR.Blue,true,true,240,20)
-- 
-- ## 2. Options
-- 
-- The following options are available (with their defaults). Only set the ones you want changed:
--
--          my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD.
--          my_ctld.CrateDistance = 35 -- List and Load crates in this radius only.
--          my_ctld.PackDistance = 35 -- Pack crates in this radius only
--          my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere.
--          my_ctld.dropAsCargoCrate = false -- Hercules only: Parachuted herc cargo is not unpacked automatically but placed as crate to be unpacked. Needs a cargo with the same name defined like the cargo that was dropped.
--          my_ctld.maximumHoverHeight = 15 -- Hover max this high to load in meters.
--          my_ctld.minimumHoverHeight = 4 -- Hover min this low to load in meters.
--          my_ctld.forcehoverload = true -- Crates (not: troops) can **only** be loaded while hovering.
--          my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows.
--          my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters).
--          my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone...
--          my_ctld.movetroopsdistance = 5000 -- .. but only if this far away (in meters)
--          my_ctld.smokedistance = 2000 -- Only smoke or flare zones if requesting player unit is this far away (in meters)
--          my_ctld.suppressmessages = false -- Set to true if you want to script your own messages.
--          my_ctld.repairtime = 300 -- Number of seconds it takes to repair a unit.
--          my_ctld.buildtime = 300 -- Number of seconds it takes to build a unit. Set to zero or nil to build instantly.
--          my_ctld.cratecountry = country.id.GERMANY -- ID of crates. Will default to country.id.RUSSIA for RED coalition setups.
--          my_ctld.allowcratepickupagain = true  -- allow re-pickup crates that were dropped.
--          my_ctld.enableslingload = false -- allow cargos to be slingloaded - might not work for all cargo types
--          my_ctld.pilotmustopendoors = false -- force opening of doors
--          my_ctld.SmokeColor = SMOKECOLOR.Red -- default color to use when dropping smoke from heli 
--          my_ctld.FlareColor = FLARECOLOR.Red -- color to use when flaring from heli
--          my_ctld.basetype = "container_cargo" -- default shape of the cargo container
--          my_ctld.droppedbeacontimeout = 600 -- dropped beacon lasts 10 minutes
--          my_ctld.usesubcats = false -- use sub-category names for crates, adds an extra menu layer in "Get Crates", useful if you have > 10 crate types.
--          my_ctld.placeCratesAhead = false -- place crates straight ahead of the helicopter, in a random way. If true, crates are more neatly sorted.
--          my_ctld.nobuildinloadzones = true -- forbid players to build stuff in LOAD zones if set to `true`
--          my_ctld.movecratesbeforebuild = true -- crates must be moved once before they can be build. Set to false for direct builds.
--          my_ctld.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- surfaces for loading back objects.
--          my_ctld.nobuildmenu = false -- if set to true effectively enforces to have engineers build/repair stuff for you.
--          my_ctld.RadioSound = "beacon.ogg" -- -- this sound will be hearable if you tune in the beacon frequency. Add the sound file to your miz.
--          my_ctld.RadioSoundFC3 = "beacon.ogg" -- this sound will be hearable by FC3 users (actually all UHF radios); change to something like "beaconsilent.ogg" and add the sound file to your miz if you don't want to annoy FC3 pilots.
--          my_ctld.enableChinookGCLoading = true -- this will effectively suppress the crate load and drop for CTLD_CARGO.Enum.STATIC types for CTLD for the Chinook
--          my_ctld.TroopUnloadDistGround = 5 -- If hovering, spawn dropped troops this far away in meters from the helo
--          my_ctld.TroopUnloadDistHover = 1.5 -- If grounded, spawn dropped troops this far away in meters from the helo
--          my_ctld.TroopUnloadDistGroundHerc = 25 -- On the ground, unload troops this far behind the Hercules
--          my_ctld.TroopUnloadDistGroundHook = 15 -- On the ground, unload troops this far behind the Chinook
--          my_ctld.TroopUnloadDistHoverHook = 5 -- When hovering, unload troops this far behind the Chinook
--          my_ctld.showstockinmenuitems = false -- When set to true, the menu lines will also show the remaining items in stock (that is, if you set any), downside is that the menu for all will be build every 30 seconds anew.
--          my_ctld.onestepmenu = false -- When set to true, the menu will create Drop and build, Get and load, Pack and remove, Pack and load, Pack. it will be a 1 step solution.
--          my_ctld.VehicleMoveFormation = AI.Task.VehicleFormation.VEE -- When a group moves to a MOVE zone, then it takes this formation. Can be a table of formations, which are then randomly chosen. Defaults to "Vee".
--          my_ctld.validateAndRepositionUnits = false -- Uses Disposition and other logic to find better ground positions for ground units avoiding trees, water, roads, runways, map scenery, statics and other units in the area. (Default is false)
--
-- ## 2.1 CH-47 Chinook support
-- 
-- The Chinook comes with the option to use the ground crew menu to load and unload cargo into the Helicopter itself for better immersion. As well, it can sling-load cargo from ground. The cargo you can actually **create**
-- from this menu is limited to contain items from the airbase or FARP's resources warehouse and can take a number of shapes (static shapes in the category of cargo) independent of their contents. If you unload this
-- kind of cargo with the ground crew, the contents will be "absorbed" into the airbase or FARP you landed at, and the cargo static will be removed after ca 2 mins. 
-- 
-- ## 2.1.1 Moose CTLD created crate cargo
-- 
-- Given the correct shape, Moose created cargo can theoretically be either loaded with the ground crew or via the F10 CTLD menu. **It is strongly stated to avoid using shapes with 
-- CTLD which can be Ground Crew loaded.**
-- Static shapes loadable *into* the Chinook and thus to **be avoided for CTLD** are at the time of writing:
-- 
--      * Ammo box (type "ammo_crate")
--      * M117 bomb crate (type name "m117_cargo")
--      * Dual shell fuel barrels (type name "barrels")
--      * UH-1H net (type name "uh1h_cargo")
--      
-- All other kinds of cargo can be sling-loaded.
--      
-- ## 2.1.2 Recommended settings
--          
--          my_ctld.basetype = "container_cargo" -- **DO NOT** change this to a base type which could also be loaded by F8/GC to avoid logic problems!
--          my_ctld.forcehoverload = false -- no hover autoload, leads to cargo complications with ground crew created cargo items
--          my_ctld.pilotmustopendoors = true -- crew must open back loading door 50% (horizontal) or more - watch out for NOT adding a back door gunner!
--          my_ctld.enableslingload = true -- will set cargo items as sling-loadable.
--          my_ctld.enableChinookGCLoading = true -- this will effectively suppress the crate load and drop for CTLD_CARGO.Enum.STATIC types for CTLD for the Chinook.
--          my_ctld.movecratesbeforebuild = true -- leave as is at the pain of building crate still **inside** of the Hook.
--          my_ctld.nobuildinloadzones = true -- don't build where you load.
--          my_ctld.ChinookTroopCircleRadius = 5 -- Radius for troops dropping in a nice circle. Adjust to your planned squad size for the Chinook.
--          
-- ## 2.2 User functions
-- 
-- ### 2.2.1 Adjust or add chopper unit-type capabilities
--  
-- Use this function to adjust what a heli type can or cannot do:
-- 
--        -- E.g. update unit capabilities for testing. Please stay realistic in your mission design.
--        -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type, up to 4000 kgs:
--        my_ctld:SetUnitCapabilities("SA342L", true, true, 8, 8, 12, 4000)
--        
--        -- Default unit type capabilities are: 
--        ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
--        ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
--        ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
--        ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
--        ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700},
--        ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
--        ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
--        ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0},
--        ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
--        ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
--        ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000},
--        ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500},
--        ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, 
--        ["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
--        ["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
--        ["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450},
--        ["OH-6A"] = {type="OH-6A", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550},
--        ["OH58D"] = {type="OH58D", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 14, cargoweightlimit = 400},
--        ["CH-47Fbl1"] = {type="CH-47Fbl1", crates=true, troops=true, cratelimit = 4, trooplimit = 31, length = 20, cargoweightlimit = 8000},
--        
-- ### 2.2.2 Activate and deactivate zones
-- 
-- Activate a zone:
-- 
--        -- Activate zone called Name of type #CTLD.CargoZoneType ZoneType:
--        my_ctld:ActivateZone(Name,CTLD.CargoZoneType.MOVE)
-- 
-- Deactivate a zone:
-- 
--        -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType:
--        my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP)
-- 
-- ## 2.2.3 Limit and manage available resources
--  
--  When adding generic cargo types, you can effectively limit how many units can be dropped/build by the players, e.g.
--  
--              -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock.
--              my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10)
--  
--  You can manually add or remove the available stock like so:
--            
--              -- Crates
--              my_ctld:AddStockCrates("Humvee", 2)
--              my_ctld:RemoveStockCrates("Humvee", 2)
--              
--              -- Troops
--              my_ctld:AddStockTroops("Anti-Air", 2)
--              my_ctld:RemoveStockTroops("Anti-Air", 2)
--  
--  Notes:
--  Troops dropped back into a LOAD zone will effectively be added to the stock. Crates lost in e.g. a heli crash are just that - lost.
--  
-- ## 2.2.4 Create own SET_GROUP to manage CTLD Pilot groups
-- 
--              -- Parameter: Set The SET_GROUP object created by the mission designer/user to represent the CTLD pilot groups.
--              -- Needs to be set before starting the CTLD instance.
--              local myset = SET_GROUP:New():FilterPrefixes("Helikopter"):FilterCoalitions("red"):FilterStart()
--              my_ctld:SetOwnSetPilotGroups(myset)
-- 
-- ## 3. Events
--
--  The class comes with a number of FSM-based events that missions designers can use to shape their mission.
--  These are:
-- 
-- ## 3.1 OnAfterTroopsPickedUp
-- 
--   This function is called when a player has loaded Troops:
--
--        function my_ctld:OnAfterTroopsPickedUp(From, Event, To, Group, Unit, Cargo)
--          ... your code here ...
--        end
-- 
-- ## 3.2 OnAfterCratesPickedUp
-- 
--    This function is called when a player has picked up crates:
--
--        function my_ctld:OnAfterCratesPickedUp(From, Event, To, Group, Unit, Cargo)
--          ... your code here ...
--        end
--  
-- ## 3.3 OnAfterTroopsDeployed
--  
--    This function is called when a player has deployed troops into the field:
--
--        function my_ctld:OnAfterTroopsDeployed(From, Event, To, Group, Unit, Troops)
--          ... your code here ...
--        end
--        
-- ## 3.4 OnAfterTroopsExtracted
--  
--    This function is called when a player has re-boarded already deployed troops from the field:
--
--        function my_ctld:OnAfterTroopsExtracted(From, Event, To, Group, Unit, Troops, Troopname)
--          ... your code here ...
--        end
--  
-- ## 3.5 OnAfterCratesDropped
--  
--    This function is called when a player has deployed crates:
--
--        function my_ctld:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable)
--          ... your code here ...
--        end
--
--- ## 3.6 OnAfterHelicopterLost
--  
--    This function is called when a player has deployed left a unit or crashed/died:
--
--        function my_ctld:OnAfterHelicopterLost(From, Event, To, Unitname, Cargotable)
--          ... your code here ...
--        end  
--  
-- ## 3.6 OnAfterCratesBuild, OnAfterCratesRepaired
--  
--    This function is called when a player has build a vehicle or FOB:
--
--        function my_ctld:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle)
--          ... your code here ...
--        end
--        
--        function my_ctld:OnAfterCratesRepaired(From, Event, To, Group, Unit, Vehicle)
--          ... your code here ...
--        end
 --  
-- ## 3.7 A simple SCORING example:
--  
--    To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport)
--
--        my_scoring = SCORING:New("Combat Transport")
--
--        function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable)
--            local points = 10
--            if Unit then
--              local PlayerName = Unit:GetPlayerName()
--              my_scoring:_AddPlayerFromUnit( Unit )
--              my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points)
--            end
--        end
--        
--        function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle)
--          local points = 5
--          if Unit then
  --          local PlayerName = Unit:GetPlayerName()
  --          my_scoring:_AddPlayerFromUnit( Unit )
  --          my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points)
--          end
--         end
--  
-- ## 4. F10 Menu structure
-- 
-- CTLD management menu is under the F10 top menu and called "CTLD"
-- 
-- ## 4.1 Manage Crates
-- 
-- Use this entry to get, load, list nearby, drop, build and repair crates. Also see options.
-- 
-- ## 4.2 Manage Troops
-- 
-- Use this entry to load, drop and extract troops. NOTE - with extract you can only load troops from the field that were deployed prior. 
-- Currently limited CTLD_CARGO troops, which are build from **one** template. Also, this will heal/complete your units as they are respawned.
-- 
-- ## 4.3 List boarded cargo
-- 
-- Lists what you have loaded. Shows load capabilities for number of crates and number of seats for troops.
-- 
-- ## 4.4 Smoke & Flare zones nearby or drop smoke, beacon or flare from Heli
-- 
-- Does what it says.
-- 
-- ## 4.5 List active zone beacons
-- 
-- Lists active radio beacons for all zones, where zones are both active and have a beacon. @see `CTLD:AddCTLDZone()`
-- 
-- ## 4.6 Show hover parameters
-- 
-- Lists hover parameters and indicates if these are curently fulfilled. Also @see options on hover heights.
-- 
-- ## 4.7 List Inventory
-- 
-- Lists inventory of available units to drop or build.
-- 
-- ## 5. Support for fixed wings
-- 
-- Basic support for the Hercules mod By Anubis has been build into CTLD, as well as Bronco and Mosquito - that is you can load/drop/build the same way and for the same objects as 
-- the helicopters (main method). 
-- To cover objects and troops which can be loaded from the groud crew Rearm/Refuel menu (F8), you need to use @{#CTLD_HERCULES.New}() and link
-- this object to your CTLD setup (alternative method). In this case, do **not** use the `Hercules_Cargo.lua` or `Hercules_Cargo_CTLD.lua` which are part of the mod 
-- in your mission!
-- 
-- ### 5.1 Create an own CTLD instance and allow the usage of the Hercules mod (main method)
--
--              local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I")
-- 
-- Enable these options for Hercules support:
--  
--              my_ctld.enableFixedWing = true
--              my_ctld.FixedMinAngels = 155 -- for troop/cargo drop via chute in meters, ca 470 ft
--              my_ctld.FixedMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft
--              my_ctld.FixedMaxSpeed = 77 -- 77mps or 270kph or 150kn
-- 
-- Hint: you can **only** airdrop from the Hercules if you are "in parameters", i.e. at or below `FixedMaxSpeed` and in the AGLFixedMinAngelseen
-- `FixedMinAngels` and `FixedMaxAngels`!
-- 
-- Also, the following options need to be set to `true`:
-- 
--              my_ctld.useprefix = true -- this is true by default and MUST BE ON. 
-- 
-- ### 5.2 Integrate Hercules ground crew (F8 Menu) loadable objects (alternative method, use either the above OR this method, NOT both!)
-- 
-- Taking another approach, integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance:
--            
--            my_ctld.enableFixedWing = false -- avoid dual loading via CTLD F10 and F8 ground crew
--            local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld)
--            
-- You also need: 
-- 
-- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate).   
-- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable.   
-- 
-- There's a **quick check output in the `dcs.log`** which tells you what's there and what not.
-- E.g.:   
-- 
--            ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING)   
--            ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING)   
--            ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING)   
--            ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK)   
--
-- Expected template names are the ones in the rounded brackets.
--
-- ### 5.2.1 Hints
-- 
-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does
-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD (see 5.1).
-- 
-- DO NOT use the "splash damage" script together with this method! Your cargo will explode on the ground!
-- 
-- There are two ways of airdropping: 
--   
-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu)   
-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu)   
-- 
-- Standard transport capabilities as per the real Hercules are:
-- 
--               ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers
--  
-- ### 5.3 Don't automatically unpack dropped cargo but drop as CTLD_CARGO
-- 
-- Cargo can be defined to be automatically dropped as crates.
--              my_ctld.dropAsCargoCrate = true -- default is false
--
-- The idea is, to have those crate behave like brought in with a helo. So any unpack restictions apply.
-- To enable those cargo drops, the cargo types must be added manually in the CTLD configuration. So when the above defined template for "Vulcan" should be used
-- as CTLD_Cargo, the following line has to be added. NoCrates, PerCrateMass, Stock, SubCategory can be configured freely.
--              my_ctld:AddCratesCargo("Vulcan",      {"Vulcan"}, CTLD_CARGO.Enum.VEHICLE, 6, 2000, nil, "SAM/AAA")
--
-- So if the Vulcan in the example now needs six crates to complete, you have to bring two Hercs with three Vulcan crates each and drop them very close together...
--
-- ## 6. Save and load back units - persistance
-- 
-- You can save and later load back units dropped or build to make your mission persistent.
-- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts.
-- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you.
-- 
-- Use the following options to manage your saves:
-- 
--              my_ctld.enableLoadSave = true -- allow auto-saving and loading of files
--              my_ctld.saveinterval = 600 -- save every 10 minutes
--              my_ctld.filename = "missionsave.csv" -- example filename
--              my_ctld.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path
--              my_ctld.eventoninject = true -- fire OnAfterCratesBuild and OnAfterTroopsDeployed events when loading (uses Inject functions)
--              my_ctld.useprecisecoordloads = true -- Instead if slightly varyiing the group position, try to maintain it as is
--  
--  Then use an initial load at the beginning of your mission:
--  
--            my_ctld:__Load(10)
--            
-- **Caveat:**
-- If you use units build by multiple templates, they will effectively double on loading. Dropped crates are not saved. Current stock is not saved.
-- 
-- ## 7. Complex example - Build a complete FARP from a CTLD crate drop
-- 
-- Prerequisites - you need to add a cargo of type FOB to your CTLD instance, for simplification reasons we call it FOB:
-- 
--            my_ctld:AddCratesCargo("FARP",{"FOB"},CTLD_CARGO.Enum.FOB,2)
--            
-- The following code will build a FARP at the coordinate the FOB was dropped and built (the UTILS function used below **does not** need a template for the statics):
--            
--          -- FARP Radio. First one has 130AM name London, next 131 name Dallas, and so forth. 
--          local FARPFreq = 129
--          local FARPName = 1  --numbers 1..10
-- 
--          local FARPClearnames = {
--            [1]="London",
--            [2]="Dallas",
--            [3]="Paris",
--            [4]="Moscow",
--            [5]="Berlin",
--            [6]="Rome",
--            [7]="Madrid",
--            [8]="Warsaw",
--            [9]="Dublin",
--            [10]="Perth",
--            }
-- 
--          function BuildAFARP(Coordinate)
--            local coord = Coordinate  --Core.Point#COORDINATE
--
--            local FarpNameNumber = ((FARPName-1)%10)+1 -- make sure 11 becomes 1 etc
--            local FName = FARPClearnames[FarpNameNumber] -- get clear namee
--  
--            FARPFreq = FARPFreq + 1
--            FARPName = FARPName + 1
--  
--            FName = FName .. " FAT COW "..tostring(FARPFreq).."AM" -- make name unique
--  
--            -- Get a Zone for loading 
--            local ZoneSpawn = ZONE_RADIUS:New("FARP "..FName,Coordinate:GetVec2(),150,false)
--  
--            -- Spawn a FARP with our little helper and fill it up with resources (10t fuel each type, 10 pieces of each known equipment)
--            UTILS.SpawnFARPAndFunctionalStatics(FName,Coordinate,ENUMS.FARPType.INVISIBLE,my_ctld.coalition,country.id.USA,FarpNameNumber,FARPFreq,radio.modulation.AM,nil,nil,nil,10,10)
-- 
--            -- add a loadzone to CTLD
--            my_ctld:AddCTLDZone("FARP "..FName,CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true)
--            local m  = MESSAGE:New(string.format("FARP %s in operation!",FName),15,"CTLD"):ToBlue() 
--          end
--
--          function my_ctld:OnAfterCratesBuild(From,Event,To,Group,Unit,Vehicle)
--            local name = Vehicle:GetName()
--            if string.find(name,"FOB",1,true) then
--              local Coord = Vehicle:GetCoordinate()
--              Vehicle:Destroy(false)
--              BuildAFARP(Coord) 
--            end
--          end
-- 
-- ## 8. Transport crates and troops with CA (Combined Arms) trucks
-- 
-- You can optionally also allow to CTLD with CA trucks and other vehicles:
-- 
--          -- Create a SET_CLIENT to capture CA vehicles steered by players
--          local truckers = SET_CLIENT:New():HandleCASlots():FilterCoalitions("blue"):FilterPrefixes("Truck"):FilterStart()
--          -- Allow CA transport
--          my_ctld:AllowCATransport(true,truckers)
--          -- Set truck capability by typename
--          my_ctld:SetUnitCapabilities("M 818", true, true, 2, 12, 9, 4500)
--          -- Alternatively set truck capability with a UNIT object
--          local GazTruck = UNIT:FindByName("GazTruck-1-1")
--          my_ctld:SetUnitCapabilities(GazTruck, true, true, 2, 12, 9, 4500)
-- 
--
-- @field #CTLD
CTLD = {
  ClassName       = "CTLD",
  verbose         = 0,
  lid             = "",
  coalition       = 1,
  coalitiontxt    = "blue",
  PilotGroups = {}, -- #GROUP_SET of heli pilots
  CtldUnits = {},   -- Table of helicopter #GROUPs
  FreeVHFFrequencies = {}, -- Table of VHF
  FreeUHFFrequencies = {}, -- Table of UHF
  FreeFMFrequencies = {}, -- Table of FM
  CargoCounter = 0,
  Cargo_Troops = {}, -- generic troops objects
  Cargo_Crates = {}, -- generic crate objects
  Loaded_Cargo = {}, -- cargo aboard units
  Spawned_Crates = {}, -- Holds objects for crates spawned generally
  Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects
  CrateDistance = 35, -- list crates in this radius
  PackDistance = 35,  -- pack crates in this radius
  debug = false,
  wpZones = {},
  dropOffZones = {},
  pickupZones  = {},
  DynamicCargo = {},
  ChinookTroopCircleRadius = 5,
  TroopUnloadDistGround = 5,
  TroopUnloadDistGroundHerc = 25,
  TroopUnloadDistGroundHook = 15,
  TroopUnloadDistHoverHook = 5,
  TroopUnloadDistHover = 1.5,
  UserSetGroup = nil,
  LoadedGroupsTable = {},
  keeploadtable = true,
  allowCATransport = false,
  VehicleMoveFormation = AI.Task.VehicleFormation.VEE,
}

------------------------------
-- DONE: Zone Checks
-- DONE: TEST Hover load and unload
-- DONE: Crate unload
-- DONE: Hover (auto-)load
-- DONE: (More) Housekeeping
-- DONE: Troops running to WP Zone
-- DONE: Zone Radio Beacons
-- DONE: Stats Running
-- DONE: Added support for Hercules
-- TODO: Possibly - either/or loading crates and troops
-- DONE: Make inject respect existing cargo types
-- DONE: Drop beacons or flares/smoke
-- DONE: Add statics as cargo
-- DONE: List cargo in stock
-- DONE: Limit of troops, crates buildable?
-- DONE: Allow saving of Troops & Vehicles
-- DONE: Adding re-packing dropped units
------------------------------

--- Radio Beacons
-- @type CTLD.ZoneBeacon
-- @field #string name -- Name of zone for the coordinate
-- @field #number frequency -- in mHz
-- @field #number modulation -- i.e.CTLD.RadioModulation.FM or CTLD.RadioModulation.AM

--- Radio Modulation
-- @type CTLD.RadioModulation
-- @field #number AM
-- @field #number FM
CTLD.RadioModulation = {
  AM = 0,
  FM = 1,
}

--- Loaded Cargo
-- @type CTLD.LoadedCargo
-- @field #number Troopsloaded
-- @field #number Cratesloaded
-- @field #table Cargo Table of #CTLD_CARGO objects

--- Zone Info.
-- @type CTLD.CargoZone
-- @field #string name Name of Zone.
-- @field #string color Smoke color for zone, e.g. SMOKECOLOR.Red.
-- @field #boolean active Active or not.
-- @field #string type Type of zone, i.e. load,drop,move,ship
-- @field #boolean hasbeacon Create and run radio beacons if active.
-- @field #table fmbeacon Beacon info as #CTLD.ZoneBeacon
-- @field #table uhfbeacon Beacon info as #CTLD.ZoneBeacon
-- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon
-- @field #number shiplength For ships - length of ship
-- @field #number shipwidth For ships - width of ship
-- @field #number timestamp For dropped beacons - time this was created

--- Zone Type Info.
-- @type CTLD.CargoZoneType
CTLD.CargoZoneType = {
  LOAD = "load",
  DROP = "drop",
  MOVE = "move",
  SHIP = "ship",
  BEACON = "beacon",
}

--- Buildable table info.
-- @type CTLD.Buildable
-- @field #string Name Name of the object.
-- @field #number Required Required crates.
-- @field #number Found Found crates.
-- @field #table Template Template names for this build.
-- @field #boolean CanBuild Is buildable or not.
-- @field #CTLD_CARGO.Enum Type Type enumerator (for moves).

--- Unit capabilities.
-- @type CTLD.UnitTypeCapabilities
-- @field #string type Unit type.
-- @field #boolean crates Can transport crate.
-- @field #boolean troops Can transport troops.
-- @field #number cratelimit Number of crates transportable.
-- @field #number trooplimit Number of troop units transportable.
-- @field #number cargoweightlimit Max loadable kgs of cargo.
CTLD.UnitTypeCapabilities = {
    ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
    ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
    ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
    ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
    ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700},
    ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
    ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
    ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0},
    ["Ka-50_3"] = {type="Ka-50_3", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0},
    ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
    ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
    ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- 19t cargo, 64 paratroopers. 
    --Actually it's longer, but the center coord is off-center of the model.
    ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
    ["UH-60L_DAP"] = {type="UH-60L_DAP", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 16, cargoweightlimit = 500}, -- UH-60L DAP is an attack helo but can do limited CSAR and CTLD
    ["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
    ["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
    ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo
    ["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450},
    ["OH-6A"] = {type="OH-6A", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550},
    ["OH58D"] = {type="OH58D", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 14, cargoweightlimit = 400},
    ["CH-47Fbl1"] = {type="CH-47Fbl1", crates=true, troops=true, cratelimit = 4, trooplimit = 31, length = 20, cargoweightlimit = 10800},
    ["MosquitoFBMkVI"] = {type="MosquitoFBMkVI", crates= true, troops=false, cratelimit = 2, trooplimit = 0, length = 13, cargoweightlimit = 1800},
    ["M 818"] = {type="M 818", crates= true, troops=true, cratelimit = 4, trooplimit = 12, length = 9, cargoweightlimit = 4500},
}

--- Allowed Fixed Wing Types
-- @type CTLD.FixedWingTypes
CTLD.FixedWingTypes = {
  ["Hercules"] = "Hercules",
  ["Bronco"] = "Bronco",
  ["Mosquito"] = "Mosquito",
}

--- CTLD class version.
-- @field #string version
CTLD.version="1.3.38"

--- Instantiate a new CTLD.
-- @param #CTLD self
-- @param #string Coalition Coalition of this CTLD. I.e. coalition.side.BLUE or coalition.side.RED or coalition.side.NEUTRAL
-- @param #table Prefixes Table of pilot prefixes.
-- @param #string Alias Alias of this CTLD for logging.
-- @return #CTLD self
function CTLD:New(Coalition, Prefixes, Alias)
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #CTLD
  
  BASE:T({Coalition, Prefixes, Alias})
  
  --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in CTLD!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end
  
  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="UNHCR"  
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="Red CTLD"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="Blue CTLD"
      end
    end
  end
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
  
  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event        -->      To State
  self:AddTransition("Stopped",       "Start",               "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",              "*")           -- CTLD status update.
  self:AddTransition("*",             "TroopsPickedUp",      "*")           -- CTLD pickup  event. 
  self:AddTransition("*",             "TroopsExtracted",     "*")           -- CTLD extract  event. 
  self:AddTransition("*",             "CratesPickedUp",      "*")           -- CTLD pickup  event.  
  self:AddTransition("*",             "TroopsDeployed",      "*")           -- CTLD deploy  event. 
  self:AddTransition("*",             "TroopsRTB",           "*")           -- CTLD deploy  event.   
  self:AddTransition("*",             "CratesDropped",       "*")           -- CTLD deploy  event.  
  self:AddTransition("*",             "CratesBuild",         "*")           -- CTLD build  event.
  self:AddTransition("*",             "CratesRepaired",      "*")           -- CTLD repair  event.
  self:AddTransition("*",             "CratesBuildStarted",  "*")           -- CTLD build  event.
  self:AddTransition("*",             "CratesRepairStarted", "*")           -- CTLD repair  event.
  self:AddTransition("*",             "CratesPacked",        "*")           -- CTLD repack  event.
  self:AddTransition("*",             "HelicopterLost",      "*")           -- CTLD lost  event.
  self:AddTransition("*",             "Load",                "*")           -- CTLD load  event.
  self:AddTransition("*",             "Loaded",              "*")           -- CTLD load  event.   
  self:AddTransition("*",             "Save",                "*")           -- CTLD save  event.      
  self:AddTransition("*",             "Stop",                "Stopped")     -- Stop FSM.
  
  -- tables
  self.PilotGroups ={}
  self.CtldUnits = {}
  
  -- Beacons
  self.FreeVHFFrequencies = {}
  self.FreeUHFFrequencies = {}
  self.FreeFMFrequencies = {}
  self.UsedVHFFrequencies = {}
  self.UsedUHFFrequencies = {}
  self.UsedFMFrequencies = {}
  
  -- radio beacons
  self.RadioSound = "beacon.ogg"
  self.RadioSoundFC3 = "beacon.ogg"
  self.RadioPath = "l10n/DEFAULT/"
  
  -- zones stuff
  self.pickupZones  = {}
  self.dropOffZones = {}
  self.wpZones = {}
  self.shipZones = {}
  self.droppedBeacons = {}
  self.droppedbeaconref = {}
  self.droppedbeacontimeout = 600
  self.useprecisecoordloads = true
  
  -- Cargo
  self.Cargo_Crates = {}
  self.Cargo_Troops = {}
  self.Cargo_Statics = {}
  self.Loaded_Cargo = {}
  self.Spawned_Crates = {}
  self.Spawned_Cargo = {}
  self.MenusDone = {}
  self.DroppedTroops = {}
  self.DroppedCrates = {}
  self.CargoCounter = 0
  self.CrateCounter = 0
  self.TroopCounter = 0
  
  -- added engineering
  self.Engineers = 0 -- #number use as counter
  self.EngineersInField = {} -- #table holds #CTLD_ENGINEERING objects
  self.EngineerSearch = 2000 -- #number search distance for crates to build or repair
  self.nobuildmenu = false -- enfore engineer build only?
  
  -- setup
  self.CrateDistance = 35 -- list/load crates in this radius
  self.PackDistance = 35 -- pack objects in this radius
  self.ExtractFactor = 3.33 -- factor for troops extraction, i.e. CrateDistance * Extractfactor
  self.prefixes = Prefixes or {"Cargoheli"}
  self.useprefix = true
  
  self.maximumHoverHeight = 15
  self.minimumHoverHeight = 4
  self.forcehoverload = true
  self.hoverautoloading = true
  self.dropcratesanywhere = false -- #1570
  self.dropAsCargoCrate = false -- Parachuted herc cargo is not unpacked automatically but placed as crate to be unpacked

  self.smokedistance = 2000
  self.movetroopstowpzone = true
  self.movetroopsdistance = 5000
  self.returntroopstobase = true -- if set to false, troops would stay after deployment inside a load zone.
  self.troopdropzoneradius = 100
  
  self.VehicleMoveFormation = AI.Task.VehicleFormation.VEE
  
  -- added support Hercules Mod
  self.enableHercules = false -- deprecated
  self.enableFixedWing = false
  self.FixedMinAngels = 165 -- for troop/cargo drop via chute
  self.FixedMaxAngels = 2000 -- for troop/cargo drop via chute
  self.FixedMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps

  self.validateAndRepositionUnits = false -- 280 kph or 150kn eq 77 mps

  -- message suppression
  self.suppressmessages = false
  
  -- time to repairor build a unit/group
  self.repairtime = 300
  self.buildtime = 300
  
  -- place spawned crates in front of aircraft
  self.placeCratesAhead = false
  
  -- country of crates spawned
  self.cratecountry = country.id.GERMANY
  
  -- for opening doors
  self.pilotmustopendoors = false
  
  if self.coalition == coalition.side.RED then
     self.cratecountry = country.id.RUSSIA
  end
  
  -- load and save dropped TROOPS
  self.enableLoadSave = false
  self.filepath = nil
  self.saveinterval = 600
  self.eventoninject = true
  self.keeploadtable = true
  self.LoadedGroupsTable = {}
  
  -- sub categories
  self.usesubcats = false
  self.subcats = {}
  self.subcatsTroop = {}
  self.showstockinmenuitems = false
  self.onestepmenu = false
  
  -- disallow building in loadzones
  self.nobuildinloadzones = true
  self.movecratesbeforebuild = true
  self.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER}
  
  -- Chinook
  self.enableChinookGCLoading = true
  self.ChinookTroopCircleRadius = 5
  
  -- User SET_GROUP
  self.UserSetGroup = nil
  
  local AliaS = string.gsub(self.alias," ","_")
  self.filename = string.format("CTLD_%s_Persist.csv",AliaS)
  
  -- allow re-pickup crates
  self.allowcratepickupagain = true
  
  -- slingload
  self.enableslingload = false
  self.basetype = "container_cargo" -- shape of the container
  
  -- Smokes and Flares
  self.SmokeColor = SMOKECOLOR.Red
  self.FlareColor = FLARECOLOR.Red
  
  for i=1,100 do
    math.random()
  end
  
  -- CA Transport
  self.allowCATransport = false -- #boolean
  self.CATransportSet = nil -- Core.Set#SET_CLIENT
  
  self:_GenerateVHFrequencies()
  self:_GenerateUHFrequencies()
  self:_GenerateFMFrequencies()
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------
  
  --- Triggers the FSM event "Start". Starts the CTLD. Initializes parameters and starts event handlers.
  -- @function [parent=#CTLD] Start
  -- @param #CTLD self

  --- Triggers the FSM event "Start" after a delay. Starts the CTLD. Initializes parameters and starts event handlers.
  -- @function [parent=#CTLD] __Start
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the CTLD and all its event handlers.
  -- @function [parent=#CTLD] Stop
  -- @param #CTLD self

  --- Triggers the FSM event "Stop" after a delay. Stops the CTLD and all its event handlers.
  -- @function [parent=#CTLD] __Stop
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#CTLD] Status
  -- @param #CTLD self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#CTLD] __Status
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.
  
  --- Triggers the FSM event "Load".
  -- @function [parent=#CTLD] Load
  -- @param #CTLD self

  --- Triggers the FSM event "Load" after a delay.
  -- @function [parent=#CTLD] __Load
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.
  
  --- Triggers the FSM event "Save".
  -- @function [parent=#CTLD] Load
  -- @param #CTLD self

  --- Triggers the FSM event "Save" after a delay.
  -- @function [parent=#CTLD] __Save
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.
  
  --- FSM Function OnBeforeTroopsPickedUp.
  -- @function [parent=#CTLD] OnBeforeTroopsPickedUp
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo troops.
  -- @return #CTLD self
  
  --- FSM Function OnBeforeTroopsExtracted.
  -- @function [parent=#CTLD] OnBeforeTroopsExtracted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops extracted.
  -- @param #string Troopname Name of the extracted group.
  -- @return #CTLD self
    
  --- FSM Function OnBeforeCratesPickedUp.
  -- @function [parent=#CTLD] OnBeforeCratesPickedUp
  -- @param #CTLD self
  -- @param #string From State .
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded!
  -- @return #CTLD self
  
   --- FSM Function OnBeforeTroopsDeployed.
  -- @function [parent=#CTLD] OnBeforeTroopsDeployed
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @return #CTLD self
  
  --- FSM Function OnBeforeCratesDropped.
  -- @function [parent=#CTLD] OnBeforeCratesDropped
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded!
  -- @return #CTLD self
  
  --- FSM Function OnBeforeCratesBuild.
  -- @function [parent=#CTLD] OnBeforeCratesBuild
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self

  --- FSM Function OnBeforeCratesRepaired.
  -- @function [parent=#CTLD] OnBeforeCratesRepaired
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired.
  -- @return #CTLD self
        
  --- FSM Function OnBeforeCratesPacked.
  -- @function [parent=#CTLD] OnBeforeCratesPacked
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate that was repacked.
  -- @return #CTLD self
    
  --- FSM Function OnBeforeTroopsRTB.
  -- @function [parent=#CTLD] OnBeforeTroopsRTB
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #string ZoneName Name of the Zone where the Troops have been RTB'd.
  -- @param Core.Zone#ZONE_Radius ZoneObject of the Zone where the Troops have been RTB'd.
  
  --- FSM Function OnAfterTroopsPickedUp.
  -- @function [parent=#CTLD] OnAfterTroopsPickedUp
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo troops.
  -- @return #CTLD self
  
  --- FSM Function OnAfterTroopsExtracted.
  -- @function [parent=#CTLD] OnAfterTroopsExtracted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops extracted.
  -- @param #string Troopname Name of the extracted group.
  -- @return #CTLD self
    
  --- FSM Function OnAfterCratesPickedUp.
  -- @function [parent=#CTLD] OnAfterCratesPickedUp
  -- @param #CTLD self
  -- @param #string From State .
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded!
  -- @return #CTLD self
  
   --- FSM Function OnAfterTroopsDeployed.
  -- @function [parent=#CTLD] OnAfterTroopsDeployed
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @return #CTLD self
  
  --- FSM Function OnAfterCratesDropped.
  -- @function [parent=#CTLD] OnAfterCratesDropped
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded!
  -- @return #CTLD self
  
  --- FSM Function OnAfterCratesBuild.
  -- @function [parent=#CTLD] OnAfterCratesBuild
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self

  --- FSM Function OnAfterCratesBuildStarted. Info event that a build has been started.
  -- @function [parent=#CTLD] OnAfterCratesBuildStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param CargoName The name of the cargo being built.
  -- @return #CTLD self

  --- FSM Function OnAfterCratesRepairStarted. Info event that a repair has been started.
  -- @function [parent=#CTLD] OnAfterCratesRepairStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @return #CTLD self

  --- FSM Function OnBeforeCratesBuildStarted. Info event that a build has been started.
  -- @function [parent=#CTLD] OnBeforeCratesBuildStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @return #CTLD self

  --- FSM Function OnBeforeCratesRepairStarted. Info event that a repair has been started.
  -- @function [parent=#CTLD] OnBeforeCratesRepairStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @return #CTLD self

  --- FSM Function OnAfterCratesRepaired.
  -- @function [parent=#CTLD] OnAfterCratesRepaired
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired.
  -- @return #CTLD self
  
  --- FSM Function OnAfterCratesPacked.
  -- @function [parent=#CTLD] OnAfterCratesPacked
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate that was repacked.
  -- @return #CTLD self
    
  --- FSM Function OnAfterTroopsRTB.
  -- @function [parent=#CTLD] OnAfterTroopsRTB
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
        
  --- FSM Function OnBeforeHelicopterLost.
  -- @function [parent=#CTLD] OnBeforeHelicopterLost
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param #string Unitname The name of the unit lost.
  -- @param #table LostCargo Table of #CTLD_CARGO object which were aboard the helicopter/transportplane lost. Can be an empty table!

  --- FSM Function OnAfterHelicopterLost.
  -- @function [parent=#CTLD] OnAfterHelicopterLost
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param #string Unitname The name of the unit lost.
  -- @param #table LostCargo Table of #CTLD_CARGO object which were aboard the helicopter/transportplane lost. Can be an empty table!
  
  --- FSM Function OnAfterLoad.
  -- @function [parent=#CTLD] OnAfterLoad
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for loading. Default is "CTLD_<alias>_Persist.csv".
  
  --- FSM Function OnAfterLoaded.
  -- @function [parent=#CTLD] OnAfterLoaded
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table LoadedGroups Table of loaded groups, each entry is a table with three values: Group, TimeStamp and CargoType.
  
  --- FSM Function OnAfterSave.
  -- @function [parent=#CTLD] OnAfterSave
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for saving. Default is "CTLD_<alias>_Persist.csv".
  
  return self
end

------------------------------------------------------------------- 
-- Helper and User Functions
------------------------------------------------------------------- 

--- (Internal) Function to get capabilities of a chopper
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The unit
-- @return #table Capabilities Table of caps
function CTLD:_GetUnitCapabilities(Unit)
  self:T(self.lid .. " _GetUnitCapabilities")
  local _unit = Unit -- Wrapper.Unit#UNIT
  local unittype = _unit:GetTypeName()
  local capabilities = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities
  if not capabilities or capabilities == {} then
    -- e.g. ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0},
    capabilities = {}
    capabilities.troops = false
    capabilities.crates = false
    capabilities.cratelimit = 0
    capabilities.trooplimit = 0
    capabilities.type = "generic"
    capabilities.length = 20
    capabilities.cargoweightlimit = 0
  end
  return capabilities
end

--- (User) Function to allow transport via Combined Arms Trucks.
-- @param #CTLD self
-- @param #boolean OnOff Switch on (true) or off (false).
-- @param Core.Set#SET_CLIENT ClientSet The CA handling client set for ground transport.
-- @return #CTLD self
function CTLD:AllowCATransport(OnOff,ClientSet)
  self.allowCATransport = OnOff -- #boolean
  self.CATransportSet = ClientSet -- Core.Set#SET_CLIENT
  return self
end

--- (Internal) Function to generate valid UHF Frequencies
-- @param #CTLD self
function CTLD:_GenerateUHFrequencies()
  self:T(self.lid .. " _GenerateUHFrequencies")
    self.FreeUHFFrequencies = {}
    self.FreeUHFFrequencies = UTILS.GenerateUHFrequencies(243,320)    
    return self
end

--- (Internal) Function to generate valid FM Frequencies
-- @param #CTLD self
function CTLD:_GenerateFMFrequencies()
  self:T(self.lid .. " _GenerateFMrequencies")
    self.FreeFMFrequencies = {}
    self.FreeFMFrequencies = UTILS.GenerateFMFrequencies()
    return self
end

--- (Internal) Populate table with available VHF beacon frequencies.
-- @param #CTLD self
function CTLD:_GenerateVHFrequencies()
  self:T(self.lid .. " _GenerateVHFrequencies")
  self.FreeVHFFrequencies = {}
  self.UsedVHFFrequencies = {}
  self.FreeVHFFrequencies = UTILS.GenerateVHFrequencies()
  return self
end

--- (User) Set drop zone radius for troop drops in meters. Minimum distance is 25m for security reasons.
-- @param #CTLD self
-- @param #number Radius The radius to use.
function CTLD:SetTroopDropZoneRadius(Radius)
  self:T(self.lid .. " SetTroopDropZoneRadius")
  local tradius = Radius or 100
  if tradius < 25 then tradius = 25 end
  self.troopdropzoneradius = tradius
  return self
end

--- (User) Add a PLAYERTASK - FSM events will check success
-- @param #CTLD self
-- @param Ops.PlayerTask#PLAYERTASK PlayerTask
-- @return #CTLD self
function CTLD:AddPlayerTask(PlayerTask)
  self:T(self.lid .. " AddPlayerTask")
  if not self.PlayerTaskQueue then
    self.PlayerTaskQueue = FIFO:New()
  end
  self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr)
  return self
end

--- (Internal) Event handler function
-- @param #CTLD self
-- @param Core.Event#EVENTDATA EventData
function CTLD:_EventHandler(EventData)
  self:T(string.format("%s Event = %d",self.lid, EventData.id))
  local event = EventData -- Core.Event#EVENTDATA
  if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then
    local _coalition = event.IniCoalition
    if _coalition ~= self.coalition then
        return --ignore!
    end
    local unitname = event.IniUnitName or "none"
    self.MenusDone[unitname] = nil
    -- check is Helicopter
    local _unit = event.IniUnit
    local _group = event.IniGroup
    if _unit:IsHelicopter() or _group:IsHelicopter() then
      local unitname = event.IniUnitName or "none"
      self.Loaded_Cargo[unitname] = nil
      self:_RefreshF10Menus()
    end
    -- Herc support
    if self:IsFixedWing(_unit) and self.enableFixedWing then
      local unitname = event.IniUnitName or "none"
      self.Loaded_Cargo[unitname] = nil
      self:_RefreshF10Menus()
    end
    -- CA support
    if _unit:IsGround() and self.allowCATransport then
      local unitname = event.IniUnitName or "none"
      self.Loaded_Cargo[unitname] = nil
      self:_RefreshF10Menus()
    end
    return
  elseif event.id == EVENTS.Land or event.id == EVENTS.Takeoff then
    local unitname = event.IniUnitName
    if self.CtldUnits[unitname] then
      local _group = event.IniGroup
      local _unit = event.IniUnit
      self:_RefreshLoadCratesMenu(_group, _unit)
    if self:IsFixedWing(_unit) and self.enableFixedWing then
      self:_RefreshDropCratesMenu(_group, _unit)
    end
    end
  elseif event.id == EVENTS.PlayerLeaveUnit or event.id == EVENTS.UnitLost then
    -- remove from pilot table
    local unitname = event.IniUnitName or "none"
    if self.CtldUnits[unitname] then
        local lostcargo = UTILS.DeepCopy(self.Loaded_Cargo[unitname] or {})
        self:__HelicopterLost(1,unitname,lostcargo)    
    end
    self.CtldUnits[unitname] = nil
    self.Loaded_Cargo[unitname] = nil
    self.MenusDone[unitname] = nil
  --elseif event.id == EVENTS.NewDynamicCargo and event.IniObjectCategory == 6 and string.match(event.IniUnitName,".+|%d%d:%d%d|PKG%d+") then
  elseif event.id == EVENTS.NewDynamicCargo then
    self:T(self.lid.."GC New Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling NEW
    --------------
    self.DynamicCargo[event.IniDynamicCargoName] = event.IniDynamicCargo
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  elseif event.id == EVENTS.DynamicCargoLoaded then
    self:T(self.lid.."GC Loaded Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling LOADING
    --------------
    local dcargo = event.IniDynamicCargo -- Wrapper.DynamicCargo#DYNAMICCARGO
    -- get client/unit object
    local client = CLIENT:FindByPlayerName(dcargo.Owner)
    if client and client:IsAlive() then
      -- add to unit load list
      local unitname = client:GetName() or "none"
      local loaded = {}
      if self.Loaded_Cargo[unitname] then
        loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
      else
        loaded = {} -- #CTLD.LoadedCargo
        loaded.Troopsloaded = 0
        loaded.Cratesloaded = 0
        loaded.Cargo = {}
      end
      loaded.Cratesloaded = loaded.Cratesloaded+1
      table.insert(loaded.Cargo,dcargo)
      self.Loaded_Cargo[unitname] = nil
      self.Loaded_Cargo[unitname] = loaded
      local Group = client:GetGroup()
      self:_SendMessage(string.format("Crate %s loaded by ground crew!",event.IniDynamicCargoName), 10, false, Group)
      self:__CratesPickedUp(1, Group, client, dcargo)
    end
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  elseif event.id == EVENTS.DynamicCargoUnloaded then
    self:T(self.lid.."GC Unload Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling UNLOADING
    --------------
    local dcargo = event.IniDynamicCargo -- Wrapper.DynamicCargo#DYNAMICCARGO
    -- get client/unit object
    local client = CLIENT:FindByPlayerName(dcargo.Owner)
    if client and client:IsAlive() then
      -- add to unit load list
      local unitname = client:GetName() or "none"
      local loaded = {}
      if self.Loaded_Cargo[unitname] then
        loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
        loaded.Cratesloaded = loaded.Cratesloaded - 1
        if loaded.Cratesloaded < 0  then loaded.Cratesloaded = 0 end
        -- TODO zap cargo from list
        local Loaded = {}
        for _,_item in pairs (loaded.Cargo or {}) do
          self:T(self.lid.."UNLOAD checking: ".._item:GetName())
          self:T(self.lid.."UNLOAD state: ".. tostring(_item:WasDropped()))
          if _item and _item:GetType() == CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName ~= _item:GetName() and not _item:WasDropped() then
            table.insert(Loaded,_item)
          else
            table.insert(Loaded,_item)
          end 
        end
        loaded.Cargo = nil
        loaded.Cargo = Loaded
        self.Loaded_Cargo[unitname] = nil
        self.Loaded_Cargo[unitname] = loaded
      else
        loaded = {} -- #CTLD.LoadedCargo
        loaded.Troopsloaded = 0
        loaded.Cratesloaded = 0
        loaded.Cargo = {}
        self.Loaded_Cargo[unitname] = loaded
      end
      local Group = client:GetGroup()
      self:_SendMessage(string.format("Crate %s unloaded by ground crew!",event.IniDynamicCargoName), 10, false, Group) 
      self:__CratesDropped(1,Group,client,{dcargo})
    end
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  elseif event.id == EVENTS.DynamicCargoRemoved then
    self:T(self.lid.."GC Remove Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling REMOVE
    --------------
    self.DynamicCargo[event.IniDynamicCargoName] = nil
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  end
  return self
end

--- (Internal) Function to message a group.
-- @param #CTLD self
-- @param #string Text The text to display.
-- @param #number Time Number of seconds to display the message.
-- @param #boolean Clearscreen Clear screen or not.
-- @param Wrapper.Group#GROUP Group The group receiving the message.
function CTLD:_SendMessage(Text, Time, Clearscreen, Group)
  self:T(self.lid .. " _SendMessage")
  if not self.suppressmessages then
    local m = MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group)
  end 
  return self
end

--- (Internal) Find a troops CTLD_CARGO object in stock
-- @param #CTLD self
-- @param #string Name of the object
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindTroopsCargoObject(Name)
  self:T(self.lid .. " _FindTroopsCargoObject")
  local cargo = nil
  for _,_cargo in pairs(self.Cargo_Troops)do
    local cargo = _cargo -- #CTLD_CARGO
    if cargo.Name == Name then
      return cargo
    end
  end
  return nil
end

--- (Internal) Find a crates CTLD_CARGO object in stock
-- @param #CTLD self
-- @param #string Name of the object
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindCratesCargoObject(Name)
  self:T(self.lid .. " _FindCratesCargoObject")
  local cargo = nil
  for _,_cargo in pairs(self.Cargo_Crates)do
    local cargo = _cargo -- #CTLD_CARGO
    if cargo.Name == Name then
      return cargo
    end
  end
  return nil
end

--- (User) Add a new fixed wing type to the list of allowed types.
-- @param #CTLD self
-- @param #string typename The typename to add. Can be handed as Wrapper.Unit#UNIT object. Do NOT forget to `myctld:SetUnitCapabilities()` for this type!
-- @return #CTLD self
function CTLD:AddAllowedFixedWingType(typename)
  if type(typename) == "string" then
    self.FixedWingTypes[typename] = typename
  elseif typename and typename.ClassName and typename:IsInstanceOf("UNIT") then
    local TypeName = typename:GetTypeName() or "none"
    self.FixedWingTypes[TypeName] = TypeName
  else
    self:E(self.lid.."No valid typename or no UNIT handed!")
  end
  return self
end

--- (User) Pre-load troops into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot!
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object
-- @param #string Troopname The name of the Troops to be loaded. Must be created prior in the CTLD setup!
-- @return #CTLD self
-- @usage
--          local client = UNIT:FindByName("Helo-1-1")
--          if client and client:IsAlive() then
--            myctld:PreloadTroops(client,"Infantry")
--          end
function CTLD:PreloadTroops(Unit,Troopname)
  self:T(self.lid .. " PreloadTroops")
  local name = Troopname or "Unknown"
  if Unit and Unit:IsAlive() then
    local cargo = self:_FindTroopsCargoObject(name)
    local group = Unit:GetGroup()
    if cargo then
      self:_LoadTroops(group,Unit,cargo,true)
    else
      self:E(self.lid.." Troops preload - Cargo Object "..name.." not found!")
    end
  end
  return self
end

--- (Internal) Pre-load crates into a helo. Do not use standalone!
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The group to load into, can be handed as Wrapper.Client#CLIENT object
-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object
-- @param #CTLD_CARGO Cargo The Cargo crate object to load
-- @param #number NumberOfCrates (Optional) Number of crates to be loaded. Default - all necessary to build this object. Might overload the helo!
-- @return #CTLD self
function CTLD:_PreloadCrates(Group, Unit, Cargo, NumberOfCrates)
    -- load crate into heli
  local group = Group -- Wrapper.Group#GROUP
  local unit = Unit -- Wrapper.Unit#UNIT
  local unitname = unit:GetName()
  -- see if this heli can load crates
  local unittype = unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local cancrates = capabilities.crates -- #boolean
  local cratelimit = capabilities.cratelimit -- #number
  if not cancrates then
    self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) 
    return self
  else
    -- have we loaded stuff already?
    local numberonboard = 0
    local massonboard = 0
    local loaded = {}
    if self.Loaded_Cargo[unitname] then
      loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
      numberonboard = loaded.Cratesloaded or 0
      massonboard = self:_GetUnitCargoMass(Unit)
    else
      loaded = {} -- #CTLD.LoadedCargo
      loaded.Troopsloaded = 0
      loaded.Cratesloaded = 0
      loaded.Cargo = {}
    end
    local crate = Cargo -- #CTLD_CARGO
    local numbercrates = NumberOfCrates or crate:GetCratesNeeded()
    for i=1,numbercrates do
      loaded.Cratesloaded = loaded.Cratesloaded + 1
      crate:SetHasMoved(true)
      crate:SetWasDropped(false)
      table.insert(loaded.Cargo, crate)
      crate.Positionable = nil
      self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group)
      --self:__CratesPickedUp(1, Group, Unit, crate)
      self.Loaded_Cargo[unitname] = loaded
      self:_UpdateUnitCargoMass(Unit)
    end 
  end
  return self
end

--- (User) Pre-load crates into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot!
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object
-- @param #string Cratesname The name of the cargo to be loaded. Must be created prior in the CTLD setup!
-- @param #number NumberOfCrates (Optional) Number of crates to be loaded. Default - all necessary to build this object. Might overload the helo!
-- @return #CTLD self
-- @usage
--          local client = UNIT:FindByName("Helo-1-1")
--          if client and client:IsAlive() then
--            myctld:PreloadCrates(client,"Humvee")
--          end
function CTLD:PreloadCrates(Unit,Cratesname,NumberOfCrates)
  self:T(self.lid .. " PreloadCrates")
  local name = Cratesname or "Unknown"
  if Unit and Unit:IsAlive() then
    local cargo = self:_FindCratesCargoObject(name)
    local group = Unit:GetGroup()
    if cargo then
      self:_PreloadCrates(group,Unit,cargo,NumberOfCrates)
    else
      self:E(self.lid.." Crates preload - Cargo Object "..name.." not found!")
    end
  end
  return self
end

--- (Internal) Function to load troops into a heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #CTLD_CARGO Cargotype
-- @param #boolean Inject
function CTLD:_LoadTroops(Group, Unit, Cargotype, Inject)
  self:T(self.lid .. " _LoadTroops")
  -- check if we have stock
  local instock = Cargotype:GetStock()
  local cgoname = Cargotype:GetName()
  local cgotype = Cargotype:GetType()
  local cgonetmass = Cargotype:GetNetMass()
  local maxloadable = self:_GetMaxLoadableMass(Unit)
  if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 and not Inject then
    -- nothing left over
    self:_SendMessage(string.format("Sorry, all %s are gone!", cgoname), 10, false, Group)
    return self
  end
  -- landed or hovering over load zone?
  local grounded = not self:IsUnitInAir(Unit)
  local hoverload = self:CanHoverLoad(Unit)
  -- check if we are in LOAD zone
  local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
  if not inzone then
    inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP)
  end
  if not Inject then
    if not inzone then
      self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group)
      if not self.debug then return self end
    elseif not grounded and not hoverload then
      self:_SendMessage("You need to land or hover in position to load!", 10, false, Group)
      if not self.debug then return self end
    elseif self.pilotmustopendoors and not  UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group)
      if not self.debug then return self end  
    end
  end
  -- load troops into heli
  local group = Group -- Wrapper.Group#GROUP
  local unit = Unit -- Wrapper.Unit#UNIT
  local unitname = unit:GetName()
  local cargotype = Cargotype -- #CTLD_CARGO
  local cratename = cargotype:GetName() -- #string
  -- see if this heli can load troops
  local unittype = unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit)
  local cantroops = capabilities.troops -- #boolean
  local trooplimit = capabilities.trooplimit -- #number
  local troopsize = cargotype:GetCratesNeeded() -- #number
  -- have we loaded stuff already?
  local numberonboard = 0
  local loaded = {}
  if self.Loaded_Cargo[unitname] then
    loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
    numberonboard = loaded.Troopsloaded or 0
  else
    loaded = {} -- #CTLD.LoadedCargo
    loaded.Troopsloaded = 0
    loaded.Cratesloaded = 0
    loaded.Cargo = {}
  end
  if troopsize + numberonboard > trooplimit then
    self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group)
    return
  elseif maxloadable < cgonetmass then
    self:_SendMessage("Sorry, that\'s too heavy to load!", 10, false, Group)
    return
  else
    self.CargoCounter = self.CargoCounter + 1
    local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass)
    self:T({cargotype=loadcargotype})
    loaded.Troopsloaded = loaded.Troopsloaded + troopsize
    table.insert(loaded.Cargo,loadcargotype)
    self.Loaded_Cargo[unitname] = loaded
    self:_SendMessage(string.format("%s boarded!", cgoname), 10, false, Group)
    self:_RefreshDropTroopsMenu(Group,Unit)
    self:__TroopsPickedUp(1,Group, Unit, Cargotype)
    self:_UpdateUnitCargoMass(Unit)
    Cargotype:RemoveStock()
  end
  return self
end

function CTLD:_FindRepairNearby(Group, Unit, Repairtype)
    self:T(self.lid .. " _FindRepairNearby")
    --self:T({Group:GetName(),Unit:GetName(),Repairtype})
    local unitcoord = Unit:GetCoordinate()
    
    -- find nearest group of deployed groups
    local nearestGroup = nil
    local nearestGroupIndex = -1
    local nearestDistance = 10000
    for k,v in pairs(self.DroppedTroops) do
      local distance = self:_GetDistance(v:GetCoordinate(),unitcoord)
      local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT
      local desc = unit:GetDesc() or nil
      if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then
        nearestGroup = v
        nearestGroupIndex = k
        nearestDistance = distance
      end
    end
    
    --self:T("Distance: ".. nearestDistance)
    
    -- found one and matching distance?  
    if nearestGroup == nil or nearestDistance > self.EngineerSearch then
      self:_SendMessage("No unit close enough to repair!", 10, false, Group)
      return nil, nil
    end
    
    local groupname = nearestGroup:GetName()
    
    -- helper to find matching template
    local function matchstring(String,Table)
      local match = false
      String = string.gsub(String,"-"," ")
      if type(Table) == "table" then       
        for _,_name in pairs (Table) do
          _name = string.gsub(_name,"-"," ")
          if string.find(String,_name) then
            match = true
            break
          end
        end
      else
        if type(String) == "string" then
          Table = string.gsub(Table,"-"," ")
          if string.find(String,Table) then match = true end
        end
      end 
      return match
    end
    
    -- walk through generics and find matching type
    local Cargotype = nil
    for k,v in pairs(self.Cargo_Crates) do
      --self:T({groupname,v.Templates,Repairtype})
      if matchstring(groupname,v.Templates) and matchstring(groupname,Repairtype) then
        Cargotype = v -- #CTLD_CARGO
        break
      end
    end

    if Cargotype == nil then
      return nil, nil
    else
      --self:T({groupname,Cargotype})
      return nearestGroup, Cargotype
    end
    
end

--- (Internal) Function to repair an object.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #table Crates Table of #CTLD_CARGO objects near the unit.
-- @param #CTLD.Buildable Build Table build object.
-- @param #number Number Number of objects in Crates (found) to limit search.
-- @param #boolean Engineering If true it is an Engineering repair.
function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering)
  self:T(self.lid .. " _RepairObjectFromCrates") 
  local build = Build -- -- #CTLD.Buildable
  local Repairtype = build.Template -- #string
  local NearestGroup, CargoType = self:_FindRepairNearby(Group,Unit,Repairtype) -- Wrapper.Group#GROUP, #CTLD_CARGO
  if NearestGroup ~= nil then
    if self.repairtime < 2 then self.repairtime = 30 end -- noob catch
    if not Engineering then
      self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group)
    end
    -- now we can build ....
    local name = CargoType:GetName()
    local required = CargoType:GetCratesNeeded()
    local template = CargoType:GetTemplates()
    local ctype = CargoType:GetType()
    local object = {} -- #CTLD.Buildable
    object.Name = CargoType:GetName()
    object.Required = required
    object.Found = required
    object.Template = template
    object.CanBuild = true
    object.Type = ctype -- #CTLD_CARGO.Enum
    self:_CleanUpCrates(Crates,Build,Number)
    local desttimer = TIMER:New(function() NearestGroup:Destroy(false) end, self)
    desttimer:Start(self.repairtime - 1)
    local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true,NearestGroup:GetCoordinate())
    buildtimer:Start(self.repairtime)
    self:__CratesRepairStarted(1,Group,Unit)
  else
    if not Engineering then
      self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group)
    else
      self:T("Can't repair this unit with " .. build.Name)
    end
  end
  return self
end

  --- (Internal) Function to extract (load from the field) troops into a heli.
  -- @param #CTLD self
  -- @param Wrapper.Group#GROUP Group
  -- @param Wrapper.Unit#UNIT Unit
  function CTLD:_ExtractTroops(Group, Unit) -- #1574 thanks to @bbirchnz!
    self:T(self.lid .. " _ExtractTroops")
    -- landed or hovering over load zone?
    local grounded = not self:IsUnitInAir(Unit)
    local hoverload = self:CanHoverLoad(Unit)
    local hassecondaries = false
    
    if not grounded and not hoverload then
      self:_SendMessage("You need to land or hover in position to load!", 10, false, Group)
      if not self.debug then return self end
    end
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to extract troops!", 10, false, Group)
      if not self.debug then return self end 
    end
    -- load troops into heli
    local unit = Unit -- Wrapper.Unit#UNIT
    local unitname = unit:GetName()
    -- see if this heli can load troops
    local unittype = unit:GetTypeName()
    local capabilities = self:_GetUnitCapabilities(Unit)
    local cantroops = capabilities.troops -- #boolean
    local trooplimit = capabilities.trooplimit -- #number
    local unitcoord = unit:GetCoordinate()
    
    -- find nearest group of deployed troops
    local nearestGroup = nil
    local nearestGroupIndex = -1
    local nearestDistance = 10000000
    local maxdistance = 0
    local nearestList = {}
    local distancekeys = {}
    local extractdistance = self.CrateDistance * self.ExtractFactor
    for k,v in pairs(self.DroppedTroops) do
      local distance = self:_GetDistance(v:GetCoordinate(),unitcoord)
      local TNow = timer.getTime()
      local vtime = v.ExtractTime or TNow-310
      if distance <= extractdistance and distance ~= -1 and (TNow - vtime > 300) then
        nearestGroup = v
        nearestGroupIndex = k
        nearestDistance = distance
        if math.floor(distance) > maxdistance then maxdistance = math.floor(distance) end
        if nearestList[math.floor(distance)] then 
          distance = maxdistance+1
          maxdistance = distance 
        end
        table.insert(nearestList, math.floor(distance), v)
        distancekeys[#distancekeys+1] = math.floor(distance)
        --self:I(string.format("Adding group %s distance %dm",nearestGroup:GetName(),distance))
      end
    end
    
    if nearestGroup == nil or nearestDistance > extractdistance then
      self:_SendMessage("No units close enough to extract!", 10, false, Group)
      return self
    end
    
    -- sort reference keys
    table.sort(distancekeys)
    
    local secondarygroups = {}
    
    for i=1,#distancekeys do
      local nearestGroup = nearestList[distancekeys[i]] -- Wrapper.Group#GROUP
          -- find matching cargo type
      local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$")
      local Cargotype = nil
      for k,v in pairs(self.Cargo_Troops) do
        local comparison = ""
        if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
        if comparison == groupType then
          Cargotype = v
          break
        end
      end
      if Cargotype == nil then
        self:_SendMessage("Can't onboard " .. groupType, 10, false, Group)
      else
      
        local troopsize = Cargotype:GetCratesNeeded() -- #number
        -- have we loaded stuff already?
        local numberonboard = 0
        local loaded = {}
        if self.Loaded_Cargo[unitname] then
          loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
          numberonboard = loaded.Troopsloaded or 0
        else
          loaded = {} -- #CTLD.LoadedCargo
          loaded.Troopsloaded = 0
          loaded.Cratesloaded = 0
          loaded.Cargo = {}
        end
        if troopsize + numberonboard > trooplimit then
          self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group)
          nearestGroup.ExtractTime = 0
          --return self
        else
          self.CargoCounter = self.CargoCounter + 1
          nearestGroup.ExtractTime = timer.getTime()
          local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, Cargotype.CargoType, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass)
          self:T({cargotype=loadcargotype})
          local running = math.floor(nearestDistance / 4)+20 -- time run to helo plus boarding
          loaded.Troopsloaded = loaded.Troopsloaded + troopsize
          table.insert(loaded.Cargo,loadcargotype)
          self.Loaded_Cargo[unitname] = loaded
          self:ScheduleOnce(running, self._SendMessage, self, string.format("%s boarded!", Cargotype.Name), 10, false, Group)
          self:_SendMessage(string.format("%s boarding!", Cargotype.Name), 10, false, Group)
          self:_RefreshDropTroopsMenu(Group,Unit)
          self:_UpdateUnitCargoMass(Unit)
          local groupname = nearestGroup:GetName()
          self:__TroopsExtracted(running,Group, Unit, nearestGroup, groupname)
          local coord = Unit:GetCoordinate() or Group:GetCoordinate() -- Core.Point#COORDINATE
          local Point
          if coord then
            local heading = unit:GetHeading() or 0
            local Angle = math.floor((heading+160)%360)
            Point = coord:Translate(8,Angle):GetVec2()
            if Point then
              nearestGroup:RouteToVec2(Point,5)
            end
          end
          -- clean up:
          hassecondaries = false
          if type(Cargotype.Templates) == "table" and Cargotype.Templates[2] then
            for _,_key in pairs (Cargotype.Templates) do
              table.insert(secondarygroups,_key)
              hassecondaries = true
            end
          end
          local destroytimer = math.random(10,20)
          --self:I("Destroying Group "..nearestGroup:GetName().." in "..destroytimer.." seconds!")
          nearestGroup:Destroy(false,destroytimer)
        end
      end
    end
    -- clean up secondary groups
    if hassecondaries == true then
      for _,_name in pairs(secondarygroups) do
        for _,_group in pairs(nearestList) do
          if _group and _group:IsAlive() then
            local groupname = string.match(_group:GetName(), "(.+)-(.+)$")
            if _name == groupname then
              _group:Destroy(false,15)
            end
          end
        end
      end
    end
    self:CleanDroppedTroops()
    return self
  end
  
--- (Internal) Function to spawn crates in front of the heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #CTLD_CARGO Cargo
-- @param #number number Number of crates to generate (for dropping)
-- @param #boolean drop If true we\'re dropping from heli rather than loading.
-- @param #boolean pack If true we\'re packing crates from a template rather than loading or dropping
function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack)
  self:T(self.lid .. " _GetCrates")
  if not drop and not pack then
    local cgoname = Cargo:GetName()
    -- check if we have stock
    local instock = Cargo:GetStock()
    if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then
      -- nothing left over
      self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group)
      return self
    end
  end
  -- check if we are in LOAD zone
  local inzone = false 
  local drop = drop or false
  local ship = nil
  local width = 20
  local distance = nil
  local zone = nil
  if not drop and not pack then 
    inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
    if not inzone then
---@diagnostic disable-next-line: cast-local-type
      inzone, ship, zone, distance, width  = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP)
    end
  elseif drop and not pack then
    if self.dropcratesanywhere then -- #1570
      inzone = true
    else
      inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP)
    end
  elseif pack and not drop then
    inzone = true
  end
  
  if not inzone then
    self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group)
    if not self.debug then return self end
  end
  
  -- Check cargo location if available
  local location = Cargo:GetLocation()
  
  if location then
    local unitcoord = Unit:GetCoordinate() or Group:GetCoordinate()
    if unitcoord then
      if not location:IsCoordinateInZone(unitcoord) then
        -- no we're not at the right spot
        self:_SendMessage("The requested cargo is not available in this zone!", 10, false, Group)
        if not self.debug then return self end
      end
    end
  end
  
  -- avoid crate spam
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local canloadcratesno = capabilities.cratelimit
  local loaddist = self.CrateDistance or 35
  local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist,true,true)
  if numbernearby >= canloadcratesno and not drop then
    self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group)
    return self
  end
  -- spawn crates in front of helicopter
  local IsHerc = self:IsFixedWing(Unit) -- Herc, Bronco and Hook load from behind
  local IsHook = self:IsHook(Unit) -- Herc, Bronco and Hook load from behind
  local IsTruck = Unit:IsGround()
  local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO
  local number = number or cargotype:GetCratesNeeded() --#number
  local cratesneeded = cargotype:GetCratesNeeded() --#number
  local cratename = cargotype:GetName()
  local cratetemplate = "Container"-- #string
  local cgotype = cargotype:GetType()
  local cgomass = cargotype:GetMass()
  local isstatic = false
  if cgotype == CTLD_CARGO.Enum.STATIC then
    cratetemplate = cargotype:GetTemplates()
    isstatic = true
  end
  -- get position and heading of heli
  local position = Unit:GetCoordinate()
  local heading = Unit:GetHeading() + 1
  local height = Unit:GetHeight()
  local droppedcargo = {}
  local cratedistance = 0
  local rheading = 0
  local angleOffNose = 0
  local addon = 0
  if IsHerc or IsHook or IsTruck then 
    -- spawn behind the Herc
    addon = 180
  end
  heading = (heading+addon)%360
  local row = 1
  local column = 1
  local initialdist = IsHerc and 16 or (capabilities.length+2) -- initial spacing of the first crates
  local startpos = position:Translate(initialdist,heading)
  if self.placeCratesAhead == true then
    cratedistance = initialdist
  end
  -- loop crates needed
  local cratecoord = nil -- Core.Point#COORDINATE
  for i=1,number do
    local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000))
    if not self.placeCratesAhead or drop == true then
      cratedistance = (i-1)*2.5 + capabilities.length
      if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end
      -- altered heading logic
      -- DONE: right standard deviation?
        if self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) then
          rheading = math.random(20,60)
        else
          rheading = UTILS.RandomGaussian(0, 30, -90, 90, 100)
        end
      rheading=math.fmod((heading+rheading),360)
      cratecoord = position:Translate(cratedistance,rheading)
    else
      cratedistance = (row-1)*6
      rheading = 90
      row = row+1
      cratecoord = startpos:Translate(cratedistance,rheading)
      if row > 4 then
        row = 1
        startpos:Translate(6,heading,nil,true)
      end
    end
    
    --local cratevec2 = cratecoord:GetVec2()
    self.CrateCounter = self.CrateCounter + 1
    local CCat, CType, CShape = Cargo:GetStaticTypeAndShape()
    local basetype = CType or self.basetype or "container_cargo"
    CCat = CCat or "Cargos"
    if isstatic then
      basetype = cratetemplate
    end
    if type(ship) == "string" then
      self:T("Spawning on ship "..ship)
      local Ship = UNIT:FindByName(ship)
      local shipcoord = Ship:GetCoordinate()
      local unitcoord = Unit:GetCoordinate()
      local dist = shipcoord:Get2DDistance(unitcoord)
      dist = dist - (20 + math.random(1,10))
      local width = width / 2
      local Offy = math.random(-width,width)
      local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
      :InitCargoMass(cgomass)
      :InitCargo(self.enableslingload)
      :InitLinkToUnit(Ship,dist,Offy,0)
      if CShape then
        spawnstatic:InitShape(CShape)
      end 
      if isstatic then
        local map=cargotype:GetStaticResourceMap()
        spawnstatic.TemplateStaticUnit.resourcePayload = map
      end
      self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
    else   
      local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
        :InitCoordinate(cratecoord)
        :InitCargoMass(cgomass)
        :InitCargo(self.enableslingload)
      if CShape then
        spawnstatic:InitShape(CShape)
      end 
      if isstatic then
        local map=cargotype:GetStaticResourceMap()
        spawnstatic.TemplateStaticUnit.resourcePayload = map
      end
      self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
    end
    local templ = cargotype:GetTemplates()
    local sorte = cargotype:GetType()
    local subcat = cargotype.Subcategory
    self.CargoCounter = self.CargoCounter + 1
    local realcargo = nil
    if drop then
                --CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory)
      realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) -- #CTLD_CARGO
      local map=cargotype:GetStaticResourceMap()
      realcargo:SetStaticResourceMap(map)
      local CCat, CType, CShape = cargotype:GetStaticTypeAndShape()
      realcargo:SetStaticTypeAndShape(CCat,CType,CShape)
      if cargotype.TypeNames then
        realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames)
      end
      table.insert(droppedcargo,realcargo)
    else
      realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat)
      local map=cargotype:GetStaticResourceMap()
      realcargo:SetStaticResourceMap(map) 
      if cargotype.TypeNames then
        realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames)
      end   
    end
    local CCat, CType, CShape = cargotype:GetStaticTypeAndShape()
    realcargo:SetStaticTypeAndShape(CCat,CType,CShape)
    table.insert(self.Spawned_Cargo, realcargo)
  end
  if not (drop or pack) then
    Cargo:RemoveStock()
  end
  local text = string.format("Crates for %s have been positioned near you!",cratename)
  if drop then
    text = string.format("Crates for %s have been dropped!",cratename)
    self:__CratesDropped(1, Group, Unit, droppedcargo)
  else
    self:_SendMessage(text, 10, false, Group)
  end
  self:_RefreshLoadCratesMenu(Group, Unit)
  return self
end

--- (Internal) Inject crates and static cargo objects.
-- @param #CTLD self
-- @param Core.Zone#ZONE Zone Zone to spawn in.
-- @param #CTLD_CARGO Cargo The cargo type to spawn.
-- @param #boolean RandomCoord Randomize coordinate.
-- @param #boolean FromLoad Create only **one** crate per cargo type, as we are re-creating dropped crates that CTLD has saved prior.
-- @return #CTLD self
function CTLD:InjectStatics(Zone, Cargo, RandomCoord, FromLoad)
  self:T(self.lid .. " InjectStatics")
  local cratecoord = Zone:GetCoordinate()
  if RandomCoord then
    cratecoord = Zone:GetRandomCoordinate(5,20)
  end
  local surface = cratecoord:GetSurfaceType()
  if surface == land.SurfaceType.WATER then
    return self
  end
  local cargotype = Cargo -- #CTLD_CARGO
  --local number = 1
  local cratesneeded = cargotype:GetCratesNeeded() --#number
  local cratetemplate = "Container"-- #string
  local cratename = cargotype:GetName()
  local cgotype = cargotype:GetType()
  local cgomass = cargotype:GetMass()
  local cratenumber = cargotype:GetCratesNeeded() or 1
  if FromLoad == true then cratenumber=1 end
  for i=1,cratenumber do
    local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000))
    local isstatic = false
    if cgotype == CTLD_CARGO.Enum.STATIC then
      cratetemplate = cargotype:GetTemplates()
      isstatic = true
    end
    local CCat,CType,CShape = cargotype:GetStaticTypeAndShape()
    local basetype = CType or self.basetype or "container_cargo"
    CCat = CCat or "Cargos"
    if isstatic then
      basetype = cratetemplate
    end
    self.CrateCounter = self.CrateCounter + 1
    local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
      :InitCargoMass(cgomass)
      :InitCargo(self.enableslingload)
      :InitCoordinate(cratecoord)
    if CShape then
      spawnstatic:InitShape(CShape)
    end
    if isstatic then
      local map = cargotype:GetStaticResourceMap()
      spawnstatic.TemplateStaticUnit.resourcePayload = map
    end
    self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
    local templ = cargotype:GetTemplates()
    local sorte = cargotype:GetType()
    cargotype.Positionable = self.Spawned_Crates[self.CrateCounter]
    table.insert(self.Spawned_Cargo, cargotype)
  end
  return self
end

--- (User) Inject static cargo objects.
-- @param #CTLD self
-- @param Core.Zone#ZONE Zone Zone to spawn in. Will be a somewhat random coordinate.
-- @param #string Template Unit(!) name of the static cargo object to be used as template.
-- @param #number Mass Mass of the static in kg.
-- @return #CTLD self
function CTLD:InjectStaticFromTemplate(Zone, Template, Mass)
  self:T(self.lid .. " InjectStaticFromTemplate")
  local cargotype = self:GetStaticsCargoFromTemplate(Template,Mass) -- #CTLD_CARGO
  self:InjectStatics(Zone,cargotype,true,true)
  return self
end

--- (Internal) Function to find and list nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_ListCratesNearby( _group, _unit)
  self:T(self.lid .. " _ListCratesNearby")
  local finddist = self.CrateDistance or 35
  local crates,number,loadedbygc,indexgc = self:_FindCratesNearby(_group,_unit, finddist,true,true) -- #table
  if number > 0 or indexgc > 0 then
    local text = REPORT:New("Crates Found Nearby:")
    text:Add("------------------------------------------------------------")
    for _,_entry in pairs (crates) do
      local entry = _entry -- #CTLD_CARGO
      local name = entry:GetName() --#string
      local dropped = entry:WasDropped()
      if dropped then
        text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass))
      else
        text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass))
      end
    end
    if text:GetCount() == 1 then
    text:Add("        N O N E")
    end
    text:Add("------------------------------------------------------------")
    if indexgc > 0 then
      text:Add("Probably ground crew loadable (F8)")
      for _,_entry in pairs (loadedbygc) do
        local entry = _entry -- #CTLD_CARGO
        local name = entry:GetName() --#string
        local dropped = entry:WasDropped()
        if dropped then
          text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass))
        else
          text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass))
        end
      end
    end
    self:_SendMessage(text:Text(), 30, true, _group) 
  else
    self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist), 10, false, _group) 
  end
  return self
end

-- (Internal) Function to find and Remove nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_RemoveCratesNearby(_group, _unit)
  self:T(self.lid.." _RemoveCratesNearby")
  local finddist=self.CrateDistance or 35
  local crates,number=self:_FindCratesNearby(_group,_unit,finddist,true,true)
  if number>0 then
    local removedIDs={}
    local text=REPORT:New("Removing Crates Found Nearby:")
    text:Add("------------------------------------------------------------")
    for _,_entry in pairs(crates)do
      local entry=_entry
      local name=entry:GetName()or"none"
      text:Add(string.format("Crate for %s, %dkg removed",name,entry.PerCrateMass))
      if entry:GetPositionable()then
        entry:GetPositionable():Destroy(false)
      end
      table.insert(removedIDs,entry:GetID())
    end
    if text:GetCount()==1 then
      text:Add("        N O N E")
    end
    text:Add("------------------------------------------------------------")
    self:_SendMessage(text:Text(),30,true,_group)
    self:_CleanupTrackedCrates(removedIDs)
    self:_RefreshLoadCratesMenu(_group,_unit)
  else
    self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist),10,false,_group)
  end
  return self
end

--- (Internal) Return distance in meters between two coordinates.
-- @param #CTLD self
-- @param Core.Point#COORDINATE _point1 Coordinate one
-- @param Core.Point#COORDINATE _point2 Coordinate two
-- @return #number Distance in meters
function CTLD:_GetDistance(_point1, _point2)
  self:T(self.lid .. " _GetDistance")
  if _point1 and _point2 then
    local distance1 = _point1:Get2DDistance(_point2)
    local distance2 = _point1:DistanceFromPointVec2(_point2)
    if distance1 and type(distance1) == "number" then
      return distance1
    elseif distance2 and type(distance2) == "number" then
      return distance2
    else
      self:E("*****Cannot calculate distance!")
      self:E({_point1,_point2})
      return -1
    end
  else
    self:E("******Cannot calculate distance!")
    self:E({_point1,_point2})
    return -1
  end
end

--- (Internal) Function to find and return nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP _group Group
-- @param Wrapper.Unit#UNIT _unit Unit
-- @param #number _dist Distance
-- @param #boolean _ignoreweight Find everything in range, ignore loadable weight
-- @param #boolean ignoretype Find everything in range, ignore loadable type name
-- @return #table Crates Table of crates
-- @return #number Number Number of crates found
-- @return #table CratesGC Table of crates possibly loaded by GC
-- @return #number NumberGC Number of crates possibly loaded by GC
function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype)
  self:T(self.lid .. " _FindCratesNearby")
  local finddist = _dist
  local location = _group:GetCoordinate()
  local existingcrates = self.Spawned_Cargo -- #table
  -- cycle
  local index = 0
  local indexg = 0
  local found = {}
  local LoadedbyGC = {}
  local loadedmass = 0
  local unittype = "none"
  local capabilities = {}
  --local maxmass = 2000
  local maxloadable = 2000
  local IsHook = self:IsHook(_unit)
  if not _ignoreweight then
    maxloadable = self:_GetMaxLoadableMass(_unit)
  end
  self:T(self.lid .. " Max loadable mass: " .. maxloadable)
  for _,_cargoobject in pairs (existingcrates) do
    local cargo = _cargoobject -- #CTLD_CARGO
    local static = cargo:GetPositionable() -- Wrapper.Static#STATIC -- crates
    local weight = cargo:GetMass() -- weight in kgs of this cargo
    local staticid = cargo:GetID()
    self:T(self.lid .. " Found cargo mass: " .. weight)
    if static and static:IsAlive() then --or cargoalive) then
      local restricthooktononstatics = self.enableChinookGCLoading and IsHook
      self:T(self.lid .. " restricthooktononstatics: " .. tostring(restricthooktononstatics))
      local cargoisstatic = cargo:GetType() == CTLD_CARGO.Enum.STATIC and true or false
      self:T(self.lid .. " Cargo is static: " .. tostring(cargoisstatic))
      local restricted = cargoisstatic and restricthooktononstatics
      self:T(self.lid .. " Loading restricted: " .. tostring(restricted))
      local staticpos = static:GetCoordinate() --or dcsunitpos
      local cando = cargo:UnitCanCarry(_unit)
      if ignoretype == true then cando = true end
      self:T(self.lid .. " Unit can carry: " .. tostring(cando))
      --- Testing
      local distance = self:_GetDistance(location,staticpos)
      self:T(self.lid .. string.format("Dist %dm/%dm | weight %dkg | maxloadable %dkg",distance,finddist,weight,maxloadable))
      if distance <= finddist and (weight <= maxloadable or _ignoreweight) and restricted == false and cando == true then 
        index = index + 1
        table.insert(found, staticid, cargo)
        maxloadable = maxloadable - weight
      end
      
    end
  end
  return found, index, LoadedbyGC, indexg
end

--- (Internal) Function to get and load nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_LoadCratesNearby(Group, Unit)
  self:T(self.lid .. " _LoadCratesNearby")
    -- load crates into heli
  local group = Group -- Wrapper.Group#GROUP
  local unit = Unit  -- Wrapper.Unit#UNIT
  local unitname = unit:GetName()
    -- see if this heli can load crates
  local unittype = unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local cancrates = capabilities.crates -- #boolean
  local cratelimit = capabilities.cratelimit -- #number
  local grounded = not self:IsUnitInAir(Unit)
  local canhoverload = self:CanHoverLoad(Unit)

  -- Door check
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to load cargo!", 10, false, Group)
    if not self.debug then return self end
  end
  --- cases -------------------------------
  -- Chopper can\'t do crates - bark & return
  -- Chopper can do crates -
  -- --> hover if forcedhover or bark and return
  -- --> hover or land if not forcedhover
  -----------------------------------------
  if not cancrates then
    self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group)
  elseif self.forcehoverload and not canhoverload then
    self:_SendMessage("Hover over the crates to pick them up!", 10, false, Group)
  elseif not grounded and not canhoverload then
    self:_SendMessage("Land or hover over the crates to pick them up!", 10, false, Group)
  else
    -- have we loaded stuff already?
    local numberonboard = 0
    local loaded        = {}
    if self.Loaded_Cargo[unitname] then
      loaded         = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
      numberonboard  = loaded.Cratesloaded or 0
    else
      loaded = {}
      loaded.Troopsloaded = 0
      loaded.Cratesloaded = 0
      loaded.Cargo = {}
    end

    -- get nearby crates
    local finddist           = self.CrateDistance or 35
    local nearcrates, number = self:_FindCratesNearby(Group,Unit,finddist,false,false)
    self:T(self.lid .. " Crates found: " .. number)

    if number == 0 and self.hoverautoloading then
      return self
    elseif number == 0 then
      self:_SendMessage("Sorry, no loadable crates nearby or max cargo weight reached!", 10, false, Group)
      return self
    elseif numberonboard == cratelimit then
      self:_SendMessage("Sorry, we are fully loaded!", 10, false, Group)
      return self
    else
      local capacity = cratelimit - numberonboard
      local crateidsloaded = {}
      local crateMap = {}

      for _, cObj in pairs(nearcrates) do
        if not cObj:HasMoved() or self.allowcratepickupagain then
          local cName = cObj:GetName() or "Unknown"
          crateMap[cName] = crateMap[cName] or {}
          table.insert(crateMap[cName], cObj)
        end
      end
      for cName, crateList in pairs(crateMap) do
        if capacity <= 0 then break end

        table.sort(crateList, function(a, b) return a:GetID() > b:GetID() end)
        local needed = crateList[1]:GetCratesNeeded() or 1
        local totalFound = #crateList
        local loadedHere = 0

        while loaded.Cratesloaded < cratelimit and loadedHere < totalFound do
          loadedHere = loadedHere + 1
          local crate = crateList[loadedHere]
          if crate and crate.Positionable then
            loaded.Cratesloaded = loaded.Cratesloaded + 1
            crate:SetHasMoved(true)
            crate:SetWasDropped(false)
            table.insert(loaded.Cargo, crate)
            table.insert(crateidsloaded, crate:GetID())
            -- destroy crate
            crate:GetPositionable():Destroy(false)
            crate.Positionable = nil
          else
            loadedHere = loadedHere - 1
            break
          end
        end

        capacity = cratelimit - loaded.Cratesloaded
        if loadedHere > 0 then
          local fullSets = math.floor(loadedHere / needed)
          local leftover = loadedHere % needed

          if needed > 1 then
            if fullSets > 0 and leftover == 0 then
              self:_SendMessage(string.format("Loaded %d %s.", fullSets, cName), 10, false, Group)
            elseif fullSets > 0 and leftover > 0 then
              self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).", fullSets, cName, leftover), 10, false, Group)
            else
              self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.", loadedHere, needed, cName), 15, false, Group)
            end
          else
            self:_SendMessage(string.format("Loaded %d %s(s).", loadedHere, cName), 10, false, Group)
          end
        end
      end
      self.Loaded_Cargo[unitname] = loaded
      self:_UpdateUnitCargoMass(Unit)
      self:_RefreshDropCratesMenu(Group, Unit)
      self:_RefreshLoadCratesMenu(Group, Unit)
      -- clean up real world crates
      self:_CleanupTrackedCrates(crateidsloaded)
      self:__CratesPickedUp(1, Group, Unit, loaded.Cargo) 
    end
  end
  return self
end


--- (Internal) Function to clean up tracked cargo crates
-- @param #CTLD self
-- @param #list crateIdsToRemove Table of IDs
-- @return self
function CTLD:_CleanupTrackedCrates(crateIdsToRemove)
  local existingcrates = self.Spawned_Cargo -- #table
  local newexcrates = {}
  for _,_crate in pairs(existingcrates) do
    local excrate = _crate -- #CTLD_CARGO
    local ID = excrate:GetID()
    local keep = true
    for _,_ID in pairs(crateIdsToRemove) do
      if ID == _ID then
        keep = false
      end
    end
    -- remove destroyed crates here too
    local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates
    if not static or not static:IsAlive() then
      keep = false
    end
    if keep then
      table.insert(newexcrates,_crate)
    end
  end
  self.Spawned_Cargo = nil
  self.Spawned_Cargo = newexcrates
  return self
end

--- (Internal) Function to get current loaded mass
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #number mass in kgs
function CTLD:_GetUnitCargoMass(Unit) 
  self:T(self.lid .. " _GetUnitCargoMass")
  if not Unit then return 0 end
  local unitname = Unit:GetName()
  local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
  local loadedmass = 0 -- #number
  if self.Loaded_Cargo[unitname] then
    local cargotable = loadedcargo.Cargo or {} -- #table
    for _,_cargo in pairs(cargotable) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
        loadedmass = loadedmass + (cargo.PerCrateMass * cargo:GetCratesNeeded())
      end
      if type ~= CTLD_CARGO.Enum.TROOPS and type ~=  CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then
        loadedmass = loadedmass + cargo.PerCrateMass
      end
      if type == CTLD_CARGO.Enum.GCLOADABLE then
        local mass = cargo:GetCargoWeight()
        loadedmass = loadedmass+mass
      end
    end
  end
  return loadedmass
end

--- (Internal) Function to calculate max loadable mass left over.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #number maxloadable Max loadable mass in kg
function CTLD:_GetMaxLoadableMass(Unit)
  self:T(self.lid .. " _GetMaxLoadableMass")
  if not Unit then return 0 end
  local loadable = 0
  local loadedmass = self:_GetUnitCargoMass(Unit)
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local maxmass = capabilities.cargoweightlimit or 2000 -- max 2 tons
  loadable = maxmass - loadedmass
  return loadable
end

--- (Internal) Function to calculate and set Unit internal cargo mass
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_UpdateUnitCargoMass(Unit)
  self:T(self.lid .. " _UpdateUnitCargoMass")
  local calculatedMass = self:_GetUnitCargoMass(Unit)
  Unit:SetUnitInternalCargo(calculatedMass)
  return self
end

--- (Internal) Function to list loaded cargo.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_ListCargo(Group, Unit)
  self:T(self.lid .. " _ListCargo")
  local unitname = Unit:GetName()
  local unittype = Unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local trooplimit = capabilities.trooplimit -- #boolean
  local cratelimit = capabilities.cratelimit -- #number
  local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
  local loadedmass = self:_GetUnitCargoMass(Unit) -- #number
  local maxloadable = self:_GetMaxLoadableMass(Unit)
  local finddist = self.CrateDistance or 35
  --local _,_,loadedgc,loadedno = self:_FindCratesNearby(Group,Unit,finddist,true)

  if self.Loaded_Cargo[unitname] then
    local no_troops = loadedcargo.Troopsloaded or 0
    local no_crates = loadedcargo.Cratesloaded or 0
    local cargotable = loadedcargo.Cargo or {} -- #table
    local report = REPORT:New("Transport Checkout Sheet")
    report:Add("------------------------------------------------------------")
    report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit))
    report:Add("------------------------------------------------------------")
    report:Add("        -- TROOPS --")
    for _,_cargo in pairs(cargotable) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then
        report:Add(string.format("Troop: %s size %d", cargo:GetName(), cargo:GetCratesNeeded()))
      end
    end
    if report:GetCount() == 4 then
      report:Add("        N O N E")
    end
    report:Add("------------------------------------------------------------")
    report:Add("       -- CRATES --")
    local cratecount = 0
    local accumCrates = {}
    for _,_cargo in pairs(cargotable or {}) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE) and (not cargo:WasDropped() or self.allowcratepickupagain) then
        local cName = cargo:GetName()
        local needed = cargo:GetCratesNeeded() or 1
        accumCrates[cName] = accumCrates[cName] or {count=0, needed=needed}
        accumCrates[cName].count = accumCrates[cName].count + 1
      end
      if type == CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then
        report:Add(string.format("GC loaded Crate: %s size 1", cargo:GetName()))
        cratecount = cratecount + 1
      end
    end
    for cName, data in pairs(accumCrates) do
      cratecount = cratecount + data.count
      report:Add(string.format("Crate: %s %d/%d", cName, data.count, data.needed))
    end
    if cratecount == 0 then
      report:Add("        N O N E")
    end
    --[[
    if loadedno > 0 then
      report:Add("------------------------------------------------------------")
      report:Add("       -- CRATES loaded via Ground Crew --")
      for _,_cargo in pairs(loadedgc or {}) do
        local cargo = _cargo -- #CTLD_CARGO
        local type = cargo:GetType() -- #CTLD_CARGO.Enum
        if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) then
          report:Add(string.format("Crate: %s size 1",cargo:GetName()))
          loadedmass = loadedmass + cargo:GetMass()
        end
      end
    end
    --]]
    report:Add("------------------------------------------------------------")
    report:Add("Total Mass: ".. loadedmass .. " kg. Loadable: "..maxloadable.." kg.")
    local text = report:Text()
    self:_SendMessage(text, 30, true, Group)
  else
    self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d | Weight limit %d kgs", trooplimit, cratelimit, maxloadable), 10, false, Group)
  end
  return self
end
--- (Internal) Function to list loaded cargo.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_ListInventory(Group, Unit)
  self:T(self.lid .. " _ListInventory")
  local unitname = Unit:GetName()
  local unittype = Unit:GetTypeName()
  local cgotypes = self.Cargo_Crates
  local trptypes = self.Cargo_Troops
  local stctypes = self.Cargo_Statics
  
  local function countcargo(cgotable)
    local counter = 0
    for _,_cgo in pairs(cgotable) do
      counter = counter + 1
    end
    return counter
  end
  
  local crateno = countcargo(cgotypes)
  local troopno = countcargo(trptypes)
  local staticno = countcargo(stctypes)
  
  if (crateno > 0 or troopno > 0 or staticno > 0) then

    local report = REPORT:New("Inventory Sheet")
    report:Add("------------------------------------------------------------")
    report:Add(string.format("Troops: %d, Cratetypes: %d",troopno,crateno+staticno))
    report:Add("------------------------------------------------------------")
    report:Add("        -- TROOPS --")
    for _,_cargo in pairs(trptypes) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
        local stockn = cargo:GetStock()
        local stock = "none"
        if stockn == -1 then 
          stock = "unlimited"
        elseif stockn > 0 then
          stock = tostring(stockn)
        end
        report:Add(string.format("Unit: %s | Soldiers: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock))
      end
    end
    if report:GetCount() == 4 then
      report:Add("        N O N E")
    end
    report:Add("------------------------------------------------------------")
    report:Add("       -- CRATES --")
    local cratecount = 0
    for _,_cargo in pairs(cgotypes) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
        local stockn = cargo:GetStock()
        local stock = "none"
        if stockn == -1 then 
          stock = "unlimited"
        elseif stockn > 0 then
          stock = tostring(stockn)
        end
        report:Add(string.format("Type: %s | Crates per Set: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock))
        cratecount = cratecount + 1
      end
    end
    -- Statics
    for _,_cargo in pairs(stctypes) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.STATIC) and not cargo:WasDropped() then
        local stockn = cargo:GetStock()
        local stock = "none"
        if stockn == -1 then 
          stock = "unlimited"
        elseif stockn > 0 then
          stock = tostring(stockn)
        end
        report:Add(string.format("Type: %s | Stock: %s",cargo:GetName(),stock))
        cratecount = cratecount + 1
      end
    end
    if cratecount == 0 then
      report:Add("        N O N E")
    end
    local text = report:Text()
    self:_SendMessage(text, 30, true, Group) 
  else
    self:_SendMessage(string.format("Nothing in stock!"), 10, false, Group) 
  end
  return self
end

--- (Internal) Function to check if a unit is an allowed fixed wing.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #boolean Outcome
function CTLD:IsFixedWing(Unit)
  local typename = Unit:GetTypeName() or "none"  
  for _,_name in pairs(self.FixedWingTypes or {}) do
    if _name and (typename==_name or string.find(typename,_name,1,true))then
      return true
    end
  end
  return false
end

--- (Internal) Function to check if a unit is a CH-47
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #boolean Outcome
function CTLD:IsHook(Unit)
    if not Unit then return false end
    local typeName = Unit:GetTypeName()
    if not typeName then return false end
    if string.find(typeName, "CH.47") then
        return true
    else
        return false
    end
end

--- (Internal) Function to set troops positions of a template to a nice circle
-- @param #CTLD self
-- @param Core.Point#COORDINATE Coordinate Start coordinate to use
-- @param #number Radius Radius to be used
-- @param #number Heading Heading starting with
-- @param #string Template The group template name
-- @return #table Positions The positions table
function CTLD:_GetUnitPositions(Coordinate,Radius,Heading,Template)
  local Positions = {}
  local template = _DATABASE:GetGroupTemplate(Template)
  --UTILS.PrintTableToLog(template)
  local numbertroops = #template.units
  local slightshift = math.abs(math.random(1,500)/100)
  local newcenter = Coordinate:Translate(Radius+slightshift,((Heading+270+math.random(1,10))%360))
  for i=1,360,math.floor(360/numbertroops) do
    local phead = ((Heading+270+i)%360)
    local post = newcenter:Translate(Radius,phead)
    local pos1 = post:GetVec2()
    local p1t = {
    x = pos1.x,
    y = pos1.y,
    heading = phead,
    }
    table.insert(Positions,p1t)
  end
  --UTILS.PrintTableToLog(Positions)
  return Positions
end

--- (Internal) Function to unload troops from heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_UnloadTroops(Group, Unit)
  self:T(self.lid .. " _UnloadTroops")
  -- check if we are in LOAD zone
  local droppingatbase = false
  local canunload = true
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group)
    if not self.debug then return self end 
  end
  local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
  if not inzone then
    inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP)
  end
  if inzone then
    droppingatbase = self.returntroopstobase
  end
  -- check for hover unload
  local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters
  local IsHerc = self:IsFixedWing(Unit)
  local IsHook = self:IsHook(Unit) 
  if IsHerc and (not IsHook) then
    -- no hover but airdrop here
    hoverunload = self:IsCorrectFlightParameters(Unit)
  end
  -- check if we\'re landed
  local grounded = not self:IsUnitInAir(Unit)
  -- Get what we have loaded
  local unitname = Unit:GetName()
  if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then
    if not droppingatbase or self.debug then
      local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
      -- looking for troops
      local cargotable = loadedcargo.Cargo
      for _,_cargo in pairs (cargotable) do
        local cargo = _cargo -- #CTLD_CARGO
        local type = cargo:GetType() -- #CTLD_CARGO.Enum
        if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
          -- unload troops
          local name = cargo:GetName() or "none"
          local temptable = cargo:GetTemplates() or {}
          local position = Group:GetCoordinate()
          local zoneradius = self.troopdropzoneradius or 100 -- drop zone radius
          local factor = 1
          if IsHerc then
            factor = cargo:GetCratesNeeded() or 1 -- spread a bit more if airdropping
            zoneradius = Unit:GetVelocityMPS() or 100
          end
          local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,zoneradius*factor)
          local randomcoord = zone:GetRandomCoordinate(10,30*factor) --:GetVec2()
          local heading = Group:GetHeading() or 0
          -- Spawn troops left from us, closer when hovering, further off when landed
          if hoverunload or grounded then
            randomcoord = Group:GetCoordinate()
            -- slightly left from us           
            local Angle = (heading+270)%360
            if IsHerc or IsHook then Angle = (heading+180)%360 end
            local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround
            if IsHerc then offset = self.TroopUnloadDistGroundHerc or 25 end
            if IsHook then  
              offset = self.TroopUnloadDistGroundHook or 15 
              if hoverunload and self.TroopUnloadDistHoverHook then
                offset = self.TroopUnloadDistHoverHook or 5
              end
            end
            randomcoord:Translate(offset,Angle,nil,true)
          end
          local tempcount = 0
          local ishook = self:IsHook(Unit)
          if ishook then tempcount = self.ChinookTroopCircleRadius or 5 end -- 10m circle for the Chinook
          for _,_template in pairs(temptable) do
            self.TroopCounter = self.TroopCounter + 1
            tempcount = tempcount+1
            local alias = string.format("%s-%d", _template, math.random(1,100000))
            local rad = 2.5+(tempcount*2)
            local Positions = self:_GetUnitPositions(randomcoord,rad,heading,_template)
            self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
              :InitDelayOff()
              :InitSetUnitAbsolutePositions(Positions)
              :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
              :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
              :SpawnFromVec2(randomcoord:GetVec2())
            self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter],type)
          end -- template loop
          cargo:SetWasDropped(true)
          -- engineering group?
          if type == CTLD_CARGO.Enum.ENGINEERS then
            self.Engineers = self.Engineers + 1
            local grpname = self.DroppedTroops[self.TroopCounter]:GetName()
            self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname)
            self:_SendMessage(string.format("Dropped Engineers %s into action!",name), 10, false, Group)
          else
            self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group)
          end
        end -- if type end
      end  -- cargotable loop
    else -- droppingatbase
        self:_SendMessage("Troops have returned to base!", 10, false, Group) 
        self:__TroopsRTB(1, Group, Unit, zonename, zone)
    end
    -- cleanup load list
    local    loaded = {} -- #CTLD.LoadedCargo
    loaded.Troopsloaded = 0
    loaded.Cratesloaded = 0
    loaded.Cargo = {}
    local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
    local cargotable = loadedcargo.Cargo or {}
    for _,_cargo in pairs (cargotable) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      local dropped = cargo:WasDropped()
      if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not dropped then
        table.insert(loaded.Cargo,_cargo)
        loaded.Cratesloaded = loaded.Cratesloaded + 1
      else
        -- add troops back to stock
        if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and droppingatbase then
          -- find right generic type
          local name = cargo:GetName()
          local gentroops = self.Cargo_Troops
          for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
            if _troop.Name == name then
              local stock = _troop:GetStock()
              -- avoid making unlimited stock limited
              if stock and tonumber(stock) >= 0 then _troop:AddStock() end
            end
          end
        end
      end
    end
    self.Loaded_Cargo[unitname] = nil
    self.Loaded_Cargo[unitname] = loaded
    self:_RefreshDropTroopsMenu(Group,Unit)
    self:_UpdateUnitCargoMass(Unit)
  else
   if IsHerc then
    self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) 
   else
    self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) 
   end
  end
  return self
end

--- (Internal) Function to unload crates from heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_UnloadCrates(Group, Unit)
    self:T(self.lid .. " _UnloadCrates")
    
    if not self.dropcratesanywhere then -- #1570
      local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP)
      if not inzone then
        self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) 
        if not self.debug then 
          return self 
        end
      end
    end
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to drop cargo!", 10, false, Group)
      if not self.debug then return self end 
    end
    local hoverunload = self:IsCorrectHover(Unit)
    local IsHerc = self:IsFixedWing(Unit)
    local IsHook = self:IsHook(Unit)
    if IsHerc and (not IsHook) then
      hoverunload = self:IsCorrectFlightParameters(Unit)
    end
    local grounded = not self:IsUnitInAir(Unit)
    local unitname = Unit:GetName()
    if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then
      local loadedcargo = self.Loaded_Cargo[unitname] or {}
      local cargotable = loadedcargo.Cargo
      local droppedCount = {}
      local neededMap = {}
      for _,_cargo in pairs (cargotable) do
        local cargo = _cargo
        local type = cargo:GetType()
        if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE and (not cargo:WasDropped() or self.allowcratepickupagain) then
          self:_GetCrates(Group, Unit, cargo, 1, true)
          cargo:SetWasDropped(true)
          cargo:SetHasMoved(true)
          local cname = cargo:GetName() or "Unknown"
          droppedCount[cname] = (droppedCount[cname] or 0) + 1
          if not neededMap[cname] then
            neededMap[cname] = cargo:GetCratesNeeded() or 1
          end
        end
      end
      for cname,count in pairs(droppedCount) do
        local needed = neededMap[cname] or 1
        if needed > 1 then
          local full = math.floor(count/needed)
          local left = count % needed
          if full > 0 and left == 0 then
            self:_SendMessage(string.format("Dropped %d %s.",full,cname),10,false,Group)
          elseif full > 0 and left > 0 then
            self:_SendMessage(string.format("Dropped %d %s(s), with %d leftover crate(s).",full,cname,left),10,false,Group)
          else
            self:_SendMessage(string.format("Dropped %d/%d crate(s) of %s.",count,needed,cname),15,false,Group)
          end
        else
          self:_SendMessage(string.format("Dropped %d %s(s).",count,cname),10,false,Group)
        end
      end
      local loaded = {}
      loaded.Troopsloaded = 0
      loaded.Cratesloaded = 0
      loaded.Cargo = {}
      for _,_cargo in pairs (cargotable) do
        local cargo = _cargo
        local type = cargo:GetType()
        local size = cargo:GetCratesNeeded()
        if type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS then
          table.insert(loaded.Cargo,_cargo)
          loaded.Troopsloaded = loaded.Troopsloaded + size
        end
        if type == CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then
          table.insert(loaded.Cargo,_cargo)
          loaded.Cratesloaded = loaded.Cratesloaded + size
        end
      end
      self.Loaded_Cargo[unitname] = nil
      self.Loaded_Cargo[unitname] = loaded
      
      self:_UpdateUnitCargoMass(Unit)
      self:_RefreshDropCratesMenu(Group,Unit)
    else
      if IsHerc then
          self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) 
      else
          self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) 
       end
    end
    return self
  end

--- (Internal) Function to build nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #boolean Engineering If true build is by an engineering team.
-- @param #boolean MultiDrop If true and not engineering or FOB, vary position a bit.
function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop)
  self:T(self.lid .. " _BuildCrates")
  -- avoid users trying to build from flying Hercs
  if self:IsFixedWing(Unit) and self.enableFixedWing and not Engineering then
    local speed = Unit:GetVelocityKMH()
    if speed > 1 then
      self:_SendMessage("You need to land / stop to build something, Pilot!", 10, false, Group) 
      return self
    end
  end
  if not Engineering and self.nobuildinloadzones then
    -- are we in a load zone?
    local inloadzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
    if inloadzone then
      self:_SendMessage("You cannot build in a loading area, Pilot!", 10, false, Group) 
      return self
    end
  end
  -- get nearby crates
  local finddist = self.CrateDistance or 35
  local crates,number = self:_FindCratesNearby(Group,Unit, finddist,true,true) -- #table
  local buildables = {}
  local foundbuilds = false
  local canbuild = false
  if number > 0 then
    -- get dropped crates
    for _,_crate in pairs(crates) do
      local Crate = _crate -- #CTLD_CARGO
      if (Crate:WasDropped() or not self.movecratesbeforebuild) and not Crate:IsRepair() and not Crate:IsStatic() then
        -- we can build these - maybe
        local name = Crate:GetName()
        local required = Crate:GetCratesNeeded()
        local template = Crate:GetTemplates()
        local ctype = Crate:GetType()
        local ccoord = Crate:GetPositionable():GetCoordinate() -- Core.Point#COORDINATE
        --local testmarker = ccoord:MarkToAll("Crate found",true,"Build Position")
        if not buildables[name] then
          local object = {} -- #CTLD.Buildable
          object.Name = name
          object.Required = required
          object.Found = 1
          object.Template = template
          object.CanBuild = false
          object.Type = ctype -- #CTLD_CARGO.Enum
          object.Coord = ccoord:GetVec2()
          buildables[name] = object
          foundbuilds = true
        else
         buildables[name].Found = buildables[name].Found + 1
         foundbuilds = true
        end
        if buildables[name].Found >= buildables[name].Required then 
           buildables[name].CanBuild = true
           canbuild = true
        end
        self:T({buildables = buildables})
      end -- end dropped
    end -- end crate loop
    -- ok let\'s list what we have
    local report = REPORT:New("Checklist Buildable Crates")
    report:Add("------------------------------------------------------------")
    for _,_build in pairs(buildables) do
      local build = _build -- Object table from above
      local name = build.Name
      local needed = build.Required
      local found = build.Found
      local txtok = "NO"
      if build.CanBuild then 
        txtok = "YES" 
      end
      local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok)
      report:Add(text)
    end -- end list buildables
    if not foundbuilds then 
      report:Add("     --- None found! ---")
      if self.movecratesbeforebuild then
        report:Add("*** Crates need to be moved before building!")
      end
    end
    report:Add("------------------------------------------------------------")
    local text = report:Text()
    if not Engineering then
      self:_SendMessage(text, 30, true, Group) 
    else
      self:T(text)
    end
    -- let\'s get going
    if canbuild then
      -- loop again
      for _,_build in pairs(buildables) do
        local build = _build -- #CTLD.Buildable
        if build.CanBuild then
          self:_CleanUpCrates(crates,build,number)
          if self.buildtime and self.buildtime > 0 then
              local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate(),MultiDrop)
              buildtimer:Start(self.buildtime)
              self:_SendMessage(string.format("Build started, ready in %d seconds!",self.buildtime),15,false,Group)
              self:__CratesBuildStarted(1,Group,Unit,build.Name) 
              self:_RefreshDropTroopsMenu(Group,Unit)
          else
            self:_BuildObjectFromCrates(Group,Unit,build,false,nil,MultiDrop)
          end
        end
      end
    end
  else
    if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end
  end -- number > 0
  return self
end

--- (Internal) Function to repair nearby vehicles / FOBs
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit

function CTLD:_PackCratesNearby(Group, Unit)
  self:T(self.lid .. " _PackCratesNearby")
  -----------------------------------------
  -- search for nearest group to player
  -- determine if group is packable
  -- generate crates and destroy group
  -----------------------------------------

  -- get nearby vehicles
  local location = Group:GetCoordinate() -- get coordinate of group using function
  local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({ZONE_RADIUS:New("TempZone", location:GetVec2(), self.PackDistance, false)}):FilterOnce() -- get all groups withing PackDistance from group using function
  -- get template name of all vehicles in zone

  -- determine if group is packable
  for _, _Group in pairs(nearestGroups.Set) do -- convert #SET_GROUP to a list of Wrapper.Group#GROUP
    for _, _Template in pairs(_DATABASE.Templates.Groups) do -- iterate through the database of templates
      if (string.match(_Group:GetName(), _Template.GroupName)) then -- check if the Wrapper.Group#GROUP near the player is in the list of templates by name
        -- generate crates and destroy group
        for _, _entry in pairs(self.Cargo_Crates) do -- iterate through #CTLD_CARGO
          if (_entry.Templates[1] == _Template.GroupName) then -- check if the #CTLD_CARGO matches the template name
            _Group:Destroy() -- if a match is found destroy the Wrapper.Group#GROUP near the player
            self:_GetCrates(Group, Unit, _entry, nil, false, true) -- spawn the appropriate crates near the player
            self:_RefreshLoadCratesMenu(Group,Unit) -- call the refresher to show the crates in the menu
            self:__CratesPacked(1,Group,Unit,_entry)
            return true
          end
        end
      end
    end
  end
    self:_SendMessage("Nothing to pack at this distance pilot!",10,false,Group)
    return false
end

--- (Internal) Function to repair nearby vehicles / FOBs
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #boolean Engineering If true, this is an engineering role
function CTLD:_RepairCrates(Group, Unit, Engineering)
  self:T(self.lid .. " _RepairCrates")
  -- get nearby crates
  local finddist = self.CrateDistance or 35
  local crates,number = self:_FindCratesNearby(Group,Unit,finddist,true,true) -- #table
  local buildables = {}
  local foundbuilds = false
  local canbuild = false
  if number > 0 then
    -- get dropped crates
    for _,_crate in pairs(crates) do
      local Crate = _crate -- #CTLD_CARGO
      if Crate:WasDropped() and Crate:IsRepair() and not Crate:IsStatic() then
        -- we can build these - maybe
        local name = Crate:GetName()
        local required = Crate:GetCratesNeeded()
        local template = Crate:GetTemplates()
        local ctype = Crate:GetType()
        if not buildables[name] then
          local object = {} -- #CTLD.Buildable
          object.Name = name
          object.Required = required
          object.Found = 1
          object.Template = template
          object.CanBuild = false
          object.Type = ctype -- #CTLD_CARGO.Enum
          buildables[name] = object
          foundbuilds = true
        else
         buildables[name].Found = buildables[name].Found + 1
         foundbuilds = true
        end
        if buildables[name].Found >= buildables[name].Required then 
           buildables[name].CanBuild = true
           canbuild = true
        end
        self:T({repair = buildables})
      end -- end dropped
    end -- end crate loop
    -- ok let\'s list what we have
    local report = REPORT:New("Checklist Repairs")
    report:Add("------------------------------------------------------------")
    for _,_build in pairs(buildables) do
      local build = _build -- Object table from above
      local name = build.Name
      local needed = build.Required
      local found = build.Found
      local txtok = "NO"
      if build.CanBuild then 
        txtok = "YES" 
      end
      local text = string.format("Type: %s | Required %d | Found %d | Can Repair %s", name, needed, found, txtok)
      report:Add(text)
    end -- end list buildables
    if not foundbuilds then report:Add("     --- None Found ---") end
    report:Add("------------------------------------------------------------")
    local text = report:Text()
    if not Engineering then
      self:_SendMessage(text, 30, true, Group) 
    else
      self:T(text)
    end
    -- let\'s get going
    if canbuild then
      -- loop again
      for _,_build in pairs(buildables) do
        local build = _build -- #CTLD.Buildable
        if build.CanBuild then
          self:_RepairObjectFromCrates(Group,Unit,crates,build,number,Engineering)
        end
      end
    end
  else
    if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end 
  end -- number > 0
  return self
end

--- (Internal) Function to actually SPAWN buildables in the mission.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Group#UNIT Unit
-- @param #CTLD.Buildable Build
-- @param #boolean Repair If true this is a repair and not a new build
-- @param Core.Point#COORDINATE RepairLocation Location for repair (e.g. where the destroyed unit was)
-- @param #boolean MultiDrop if true and not a repair, vary location a bit if not a FOB
function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,MultiDrop)
  self:T(self.lid .. " _BuildObjectFromCrates")
  -- Spawn-a-crate-content
  if Group and Group:IsAlive() or (RepairLocation and not Repair) then
    --local position = Unit:GetCoordinate() or Group:GetCoordinate()
    --local unitname = Unit:GetName() or Group:GetName() or "Unknown"
    local name = Build.Name
    local ctype = Build.Type -- #CTLD_CARGO.Enum
    local canmove = false
    if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end
    if ctype == CTLD_CARGO.Enum.STATIC then 
      return self 
    end
    local temptable = Build.Template or {}
    if type(temptable) == "string" then 
      temptable = {temptable}
    end
    local zone = nil -- Core.Zone#ZONE_RADIUS
    if RepairLocation and not Repair then
      -- timed build
      zone = ZONE_RADIUS:New(string.format("Build zone-%d",math.random(1,10000)),RepairLocation:GetVec2(),100)
    else
      zone = ZONE_GROUP:New(string.format("Unload zone-%d",math.random(1,10000)),Group,100)
    end
    --local randomcoord = zone:GetRandomCoordinate(35):GetVec2()
    local randomcoord = Build.Coord or zone:GetRandomCoordinate(35):GetVec2()
    if MultiDrop and (not Repair) and canmove then
      -- coordinate may be the same, avoid
      local randomcoord = zone:GetRandomCoordinate(35):GetVec2()
    end
    if Repair then
      randomcoord = RepairLocation:GetVec2()
    end
    for _,_template in pairs(temptable) do
      self.TroopCounter = self.TroopCounter + 1
      local alias = string.format("%s-%d", _template, math.random(1,100000))
      if canmove then
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
          --:InitRandomizeUnits(true,20,2)
          :InitDelayOff()
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
          :SpawnFromVec2(randomcoord)
      else -- don't random position of e.g. SAM units build as FOB
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
          :InitDelayOff()
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
          :SpawnFromVec2(randomcoord)
      end
      if Repair then
        self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter])
      else
        self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter])
      end
    end -- template loop
  else
    self:T(self.lid.."Group KIA while building!")
  end
  return self
end

--- (Internal) Function to get a vehicle formation for a moving group
-- @param #CTLD self
-- @return #string Formation
function CTLD:_GetVehicleFormation()
  local VehicleMoveFormation = self.VehicleMoveFormation or AI.Task.VehicleFormation.VEE
  if type(self.VehicleMoveFormation)=="table" then
    VehicleMoveFormation = self.VehicleMoveFormation[math.random(1,#self.VehicleMoveFormation)]
  end
  return VehicleMoveFormation
end

--- (Internal) Function to move group to WP zone.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The Group to move.
function CTLD:_MoveGroupToZone(Group)
  self:T(self.lid .. " _MoveGroupToZone")
  local groupname = Group:GetName() or "none"
  local groupcoord = Group:GetCoordinate()
  -- Get closest zone of type
  local outcome, name, zone, distance  = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE)
  self:T({canmove=outcome, name=name, zone=zone, dist=distance,max=self.movetroopsdistance})
  if (distance <= self.movetroopsdistance) and outcome == true and zone~= nil then
    -- yes, we can ;)
    local groupname = Group:GetName()
    local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE
    local formation = self:_GetVehicleFormation()
    --local coordinate = zonecoord:GetVec2()
    Group:SetAIOn()
    Group:OptionAlarmStateAuto()
    Group:OptionDisperseOnAttack(30)
    Group:OptionROEOpenFire()
    Group:RouteGroundTo(zonecoord,25,formation)
    end
  return self
end

--- (Internal) Housekeeping - Cleanup crates when build
-- @param #CTLD self
-- 
-- @param #table Crates Table of #CTLD_CARGO objects near the unit.
-- @param #CTLD.Buildable Build Table build object.
-- @param #number Number Number of objects in Crates (found) to limit search.
function CTLD:_CleanUpCrates(Crates,Build,Number)
  self:T(self.lid .. " _CleanUpCrates")
  -- clean up real world crates
  local build = Build -- #CTLD.Buildable
  local existingcrates = self.Spawned_Cargo -- #table of exising crates
  local newexcrates = {}
  -- get right number of crates to destroy
  local numberdest = Build.Required
  local nametype = Build.Name
  local found = 0
  local rounds = Number
  local destIDs = {}
  
  -- loop and find matching IDs in the set
  for _,_crate in pairs(Crates) do
    local nowcrate = _crate -- #CTLD_CARGO
    local name = nowcrate:GetName()
    local thisID = nowcrate:GetID()
    if name == nametype then -- matching crate type
      table.insert(destIDs,thisID)
      found = found + 1
      nowcrate:GetPositionable():Destroy(false)
      nowcrate.Positionable = nil
      nowcrate.HasBeenDropped = false
    end
    if found == numberdest then break end -- got enough
  end
  -- loop and remove from real world representation
  self:_CleanupTrackedCrates(destIDs)
  return self
end

--- (Internal) Helper - Drop **all** loaded crates nearby and build them.
-- @param Wrapper.Group#GROUP Group The calling group
-- @param Wrapper.Unit#UNIT  Unit  The calling unit
function CTLD:_DropAndBuild(Group,Unit)
    if self.nobuildinloadzones then
      if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) then
        self:_SendMessage("You cannot build in a loading area, Pilot!",10,false,Group)
        return self
      end
    end
    self:_UnloadCrates(Group,Unit)
    timer.scheduleFunction(function() self:_BuildCrates(Group,Unit,false,true) end,{},timer.getTime()+1)
  end
  
  --- (Internal) Helper - Drop a **single** crate set and build it.
-- @param Wrapper.Group#GROUP Group     The calling group
-- @param Wrapper.Unit#UNIT  Unit       The calling unit
-- @param number             setIndex   Index of the crate-set to drop
  function CTLD:_DropSingleAndBuild(Group,Unit,setIndex)
    if self.nobuildinloadzones then
      if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) then
        self:_SendMessage("You cannot build in a loading area, Pilot!",10,false,Group)
        return self
      end
    end
    self:_UnloadSingleCrateSet(Group,Unit,setIndex)
    timer.scheduleFunction(function() self:_BuildCrates(Group,Unit,false) end,{},timer.getTime()+1)
  end

--- (Internal) Helper - Pack crates near the unit and load them.
-- @param Wrapper.Group#GROUP Group  The calling group
-- @param Wrapper.Unit#UNIT  Unit    The calling unit
function CTLD:_PackAndLoad(Group,Unit)
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group)
      return self
    end
    if not self:_PackCratesNearby(Group,Unit) then
        return self
      end
    timer.scheduleFunction(function() self:_LoadCratesNearby(Group,Unit) end,{},timer.getTime()+1)
    return self
  end

--- (Internal) Helper - Pack crates near the unit and then remove them.
-- @param Wrapper.Group#GROUP Group  The calling group
-- @param Wrapper.Unit#UNIT  Unit    The calling unit
function CTLD:_PackAndRemove(Group,Unit)
    if not self:_PackCratesNearby(Group,Unit) then
        return self
      end
        timer.scheduleFunction(function() self:_RemoveCratesNearby(Group,Unit) end,{},timer.getTime()+1)
    return self
end

--- (Internal) Helper - get and load in one step
-- @param Wrapper.Group#GROUP Group  The calling group
-- @param Wrapper.Unit#UNIT  Unit    The calling unit
function CTLD:_GetAndLoad(Group,Unit,cargoObj)
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
        self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group)
        return self
    end
    self:_GetCrates(Group,Unit,cargoObj)

    timer.scheduleFunction(function() self:_LoadSingleCrateSet(Group,Unit,cargoObj.Name) end,{},timer.getTime()+1)
end

-- @param Wrapper.Group#GROUP Group The player’s group that triggered the action
-- @param Wrapper.Unit#UNIT  Unit The unit performing the pack-and-load  
function CTLD:_GetAllAndLoad(Group,Unit)
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
        self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group)
        return self
    end

    timer.scheduleFunction(function() self:_LoadCratesNearby(Group,Unit) end,{},timer.getTime()+1)
end

--- (Internal) Housekeeping - Function to refresh F10 menus.
-- @param #CTLD self
-- @return #CTLD self
function CTLD:_RefreshF10Menus()
    self:T(self.lid .. " _RefreshF10Menus")
    self.onestepmenu = self.onestepmenu or false                                            -- hybrid toggle (default = false)
  
    -- 1) Gather all the pilot groups from our Set
    local PlayerSet   = self.PilotGroups
    local PlayerTable = PlayerSet:GetSetObjects()
  
    -- 2) Rebuild the self.CtldUnits table
    local _UnitList = {}
    for _, groupObj in pairs(PlayerTable) do
      local firstUnit = groupObj:GetFirstUnitAlive()
      if firstUnit then
        if firstUnit:IsPlayer() then
          if firstUnit:IsHelicopter() or (self.enableFixedWing and self:IsFixedWing(firstUnit)) then
            local _unit = firstUnit:GetName()
            _UnitList[_unit] = _unit
          end
        end
      end
    end
    
    -- 3) CA Units
    if self.allowCATransport and self.CATransportSet then
      for _,_clientobj in pairs(self.CATransportSet.Set) do
        local client = _clientobj -- Wrapper.Client#CLIENT
        if client:IsGround() then
          local cname = client:GetName()
          self:T(self.lid.."Adding: "..cname)
          _UnitList[cname] = cname
        end
      end
    end
    
    self.CtldUnits = _UnitList
  
    -- subcats?
    if self.usesubcats then
     for _id,_cargo in pairs(self.Cargo_Crates) do
      local entry = _cargo -- #CTLD_CARGO
      if not self.subcats[entry.Subcategory] then
        self.subcats[entry.Subcategory] = entry.Subcategory
      end
     end
     for _id,_cargo in pairs(self.Cargo_Statics) do
      local entry = _cargo -- #CTLD_CARGO
      if not self.subcats[entry.Subcategory] then
        self.subcats[entry.Subcategory] = entry.Subcategory
      end
     end
     for _id,_cargo in pairs(self.Cargo_Troops) do
      local entry = _cargo -- #CTLD_CARGO
      if not self.subcatsTroop[entry.Subcategory] then
        self.subcatsTroop[entry.Subcategory] = entry.Subcategory
      end
     end
    end
  
    local menucount = 0
    local menus = {}
    for _, _unitName in pairs(self.CtldUnits) do
      if (not self.MenusDone[_unitName]) or (self.showstockinmenuitems == true) then
        self:T(self.lid.."Menu not done yet for ".._unitName)
        local _unit  = UNIT:FindByName(_unitName)
        if not _unit and self.allowCATransport then
          _unit = CLIENT:FindByName(_unitName)
        end
        if _unit and _unit:IsAlive() then
          local _group = _unit:GetGroup()
          if _group then
            self:T(self.lid.."Unit and Group exist")
            local capabilities = self:_GetUnitCapabilities(_unit)
            local cantroops  = capabilities.troops
            local cancrates  = capabilities.crates
            local unittype   = _unit:GetTypeName()
            local isHook     = self:IsHook(_unit)
            local nohookswitch = true
            --local nohookswitch = not (isHook and self.enableChinookGCLoading)
            -- Clear old topmenu if it existed
            if _group.CTLDTopmenu then
              _group.CTLDTopmenu:Remove()
              _group.CTLDTopmenu = nil
            end
            local toptroops = nil
            local topcrates = nil
            local topmenu = MENU_GROUP:New(_group, "CTLD", nil)
            _group.CTLDTopmenu = topmenu
  
            if cantroops then
              local toptroops  = MENU_GROUP:New(_group, "Manage Troops", topmenu)
              local troopsmenu = MENU_GROUP:New(_group, "Load troops", toptroops)
              _group.MyTopTroopsMenu = toptroops
              
              if self.usesubcats then
                local subcatmenus = {}
                for catName, _ in pairs(self.subcatsTroop) do
                  subcatmenus[catName] = MENU_GROUP:New(_group, catName, troopsmenu)
                end
                for _, cargoObj in pairs(self.Cargo_Troops) do
                  if not cargoObj.DontShowInMenu then
                    local stock = cargoObj:GetStock()
                    local menutext = cargoObj.Name
                    if (stock >= 0) and (self.showstockinmenuitems == true) then menutext = menutext.." ["..stock.."]" end
                    MENU_GROUP_COMMAND:New(_group, menutext, subcatmenus[cargoObj.Subcategory], self._LoadTroops, self, _group, _unit, cargoObj)
                  end
                end
              else
                for _, cargoObj in pairs(self.Cargo_Troops) do
                  if not cargoObj.DontShowInMenu then
                    local stock = cargoObj:GetStock()
                    local menutext = cargoObj.Name
                    if (stock >= 0) and (self.showstockinmenuitems == true) then menutext = menutext.." ["..stock.."]" end
                    MENU_GROUP_COMMAND:New(_group, menutext, troopsmenu, self._LoadTroops, self, _group, _unit, cargoObj)
                  end
                end
              end
              local dropTroopsMenu=MENU_GROUP:New(_group,"Drop Troops",toptroops):Refresh()
              MENU_GROUP_COMMAND:New(_group,"Drop ALL troops",dropTroopsMenu,self._UnloadTroops,self,_group,_unit):Refresh()
              MENU_GROUP_COMMAND:New(_group,"Extract troops",toptroops,self._ExtractTroops,self,_group,_unit):Refresh()
              local uName=_unit:GetName()
              local loadedData=self.Loaded_Cargo[uName]
              if loadedData and loadedData.Cargo then
                for i,cargoObj in ipairs(loadedData.Cargo) do
                  if cargoObj and (cargoObj:GetType()==CTLD_CARGO.Enum.TROOPS or cargoObj:GetType()==CTLD_CARGO.Enum.ENGINEERS) and not cargoObj:WasDropped() then
                    local name=cargoObj:GetName() or "Unknown"
                    local needed=cargoObj:GetCratesNeeded() or 1
                    local cID=cargoObj:GetID()
                    local line=string.format("Drop: %s",name,needed,cID)
                    MENU_GROUP_COMMAND:New(_group,line,dropTroopsMenu,self._UnloadSingleTroopByID,self,_group,_unit,cID):Refresh()
                  end
                end
              end
            end
            if cancrates then
              local topcrates  = MENU_GROUP:New(_group, "Manage Crates", topmenu)
              _group.MyTopCratesMenu = topcrates
  
              -- Build the “Get Crates” sub-menu items
              local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates)
  
              if self.onestepmenu then
                if self.usesubcats then
                  local subcatmenus = {}
                  for catName,_ in pairs(self.subcats) do
                    subcatmenus[catName] = MENU_GROUP:New(_group,catName,cratesmenu)
                  end
                  for _,cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,subcatmenus[cargoObj.Subcategory])
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                    end
                  end
                  for _,cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,subcatmenus[cargoObj.Subcategory])
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                    end
                  end
                else
                  for _,cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,cratesmenu)
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)                      
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                    end
                  end
                  for _,cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,cratesmenu)
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                      
                    end
                  end
                end
              else
                if self.usesubcats == true then
                  local subcatmenus = {}
                  for catName, _ in pairs(self.subcats) do
                    subcatmenus[catName] = MENU_GROUP:New(_group, catName, cratesmenu)         -- fixed variable case
                  end
                  for _, cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, subcatmenus[cargoObj.Subcategory], self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                  for _, cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, subcatmenus[cargoObj.Subcategory], self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                else
                  for _, cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, cratesmenu, self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                  for _, cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, cratesmenu, self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                end
              end
  
              local loadCratesMenu=MENU_GROUP:New(_group,"Load Crates",topcrates)
              _group.MyLoadCratesMenu=loadCratesMenu
              MENU_GROUP_COMMAND:New(_group,"Load ALL",loadCratesMenu,self._LoadCratesNearby,self,_group,_unit)
              MENU_GROUP_COMMAND:New(_group,"Show loadable crates",loadCratesMenu,self._RefreshLoadCratesMenu,self,_group,_unit)
  
              local dropCratesMenu = MENU_GROUP:New(_group,"Drop Crates",topcrates)
              topcrates.DropCratesMenu = dropCratesMenu
  
              if not self.nobuildmenu then
                MENU_GROUP_COMMAND:New(_group, "Build crates", topcrates, self._BuildCrates, self, _group, _unit)
                MENU_GROUP_COMMAND:New(_group, "Repair", topcrates, self._RepairCrates, self, _group, _unit):Refresh()
              end
  
              local removecratesmenu = MENU_GROUP:New(_group, "Remove crates", topcrates)
              MENU_GROUP_COMMAND:New(_group, "Remove crates nearby", removecratesmenu, self._RemoveCratesNearby, self, _group, _unit)
  
              if self.onestepmenu then
                local mPack=MENU_GROUP:New(_group,"Pack crates",topcrates)
                MENU_GROUP_COMMAND:New(_group,"Pack",mPack,self._PackCratesNearby,self,_group,_unit)
                MENU_GROUP_COMMAND:New(_group,"Pack and Load",mPack,self._PackAndLoad,self,_group,_unit)
                MENU_GROUP_COMMAND:New(_group,"Pack and Remove",mPack,self._PackAndRemove,self,_group,_unit)
                MENU_GROUP_COMMAND:New(_group, "List crates nearby", topcrates, self._ListCratesNearby, self, _group, _unit)
              else
                MENU_GROUP_COMMAND:New(_group, "Pack crates", topcrates, self._PackCratesNearby, self, _group, _unit)
                MENU_GROUP_COMMAND:New(_group, "List crates nearby", topcrates, self._ListCratesNearby, self, _group, _unit)
              end
  
              local uName = _unit:GetName()
              local loadedData = self.Loaded_Cargo[uName]
              if loadedData and loadedData.Cargo then
                local cargoByName = {}
                for _, cgo in pairs(loadedData.Cargo) do
                  if cgo and (not cgo:WasDropped()) then
                    local cname   = cgo:GetName()
                    local cneeded = cgo:GetCratesNeeded()
                    cargoByName[cname] = cargoByName[cname] or { count=0, needed=cneeded }
                    cargoByName[cname].count = cargoByName[cname].count + 1
                  end
                end
                for name, info in pairs(cargoByName) do
                  local line = string.format("Drop %s (%d/%d)", name, info.count, info.needed)
                  MENU_GROUP_COMMAND:New(_group, line, dropCratesMenu, self._UnloadSingleCrateSet, self, _group, _unit, name)
                end
              end
            end
  
            -----------------------------------------------------
            -- Misc sub‐menus
            -----------------------------------------------------
            MENU_GROUP_COMMAND:New(_group, "List boarded cargo", topmenu, self._ListCargo, self, _group, _unit)
            MENU_GROUP_COMMAND:New(_group, "Inventory", topmenu, self._ListInventory, self, _group, _unit)
            MENU_GROUP_COMMAND:New(_group, "List active zone beacons", topmenu, self._ListRadioBeacons, self, _group, _unit)
  
            local smoketopmenu = MENU_GROUP:New(_group, "Smokes, Flares, Beacons", topmenu)
            MENU_GROUP_COMMAND:New(_group, "Smoke zones nearby", smoketopmenu, self.SmokeZoneNearBy, self, _unit, false)
            local smokeself = MENU_GROUP:New(_group, "Drop smoke now", smoketopmenu)
            MENU_GROUP_COMMAND:New(_group, "Red smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Red)
            MENU_GROUP_COMMAND:New(_group, "Blue smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Blue)
            MENU_GROUP_COMMAND:New(_group, "Green smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Green)
            MENU_GROUP_COMMAND:New(_group, "Orange smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Orange)
            MENU_GROUP_COMMAND:New(_group, "White smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.White)
  
            MENU_GROUP_COMMAND:New(_group, "Flare zones nearby", smoketopmenu, self.SmokeZoneNearBy, self, _unit, true)
            MENU_GROUP_COMMAND:New(_group, "Fire flare now", smoketopmenu, self.SmokePositionNow, self, _unit, true)
            MENU_GROUP_COMMAND:New(_group, "Drop beacon now", smoketopmenu, self.DropBeaconNow, self, _unit):Refresh()
  
            if self:IsFixedWing(_unit) then
              MENU_GROUP_COMMAND:New(_group, "Show flight parameters", topmenu, self._ShowFlightParams, self, _group, _unit):Refresh()
            else
              MENU_GROUP_COMMAND:New(_group, "Show hover parameters", topmenu, self._ShowHoverParams, self, _group, _unit):Refresh()
            end
  
            -- Mark we built the menu
            self.MenusDone[_unitName] = true
            self:_RefreshLoadCratesMenu(_group,_unit)
            self:_RefreshDropCratesMenu(_group,_unit)
  
          end -- if _group
        end -- if _unit
      else
        self:T(self.lid .. " Menus already done for this group!")
      end
    end -- for all pilot units
  
    return self
  end
  
--- (Internal) Function to refresh the menu for load crates. Triggered from land/getcrate/pack and more
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @return #CTLD self
function CTLD:_RefreshLoadCratesMenu(Group,Unit)
    if not Group.MyLoadCratesMenu then return end
    Group.MyLoadCratesMenu:RemoveSubMenus()
  
    local d=self.CrateDistance or 35
    local nearby,n=self:_FindCratesNearby(Group,Unit,d,true,true)
    if n==0 then
      MENU_GROUP_COMMAND:New(Group,"No crates found! Rescan?",Group.MyLoadCratesMenu,function() self:_RefreshLoadCratesMenu(Group,Unit) end)
      return
    end
    MENU_GROUP_COMMAND:New(Group,"Load ALL",Group.MyLoadCratesMenu,self._LoadCratesNearby,self,Group,Unit)
  
    local cargoByName={}
    for _,crate in pairs(nearby) do
      local name=crate:GetName()
      cargoByName[name]=cargoByName[name] or{}
      table.insert(cargoByName[name],crate)
    end
  
    local lineIndex=1
    for cName,list in pairs(cargoByName) do
      local needed=list[1]:GetCratesNeeded() or 1
      table.sort(list,function(a,b)return a:GetID()<b:GetID() end)
      local i=1
      while i<=#list do
        local left=#list-i+1
        local label
        if left>=needed then
          label=string.format("%d. Load %s",lineIndex,cName)
          i=i+needed
        else
          label=string.format("%d. Load %s (%d/%d)",lineIndex,cName,left,needed)
          i=#list+1
        end
        MENU_GROUP_COMMAND:New(Group,label,Group.MyLoadCratesMenu,self._LoadSingleCrateSet,self,Group,Unit,cName)
        lineIndex=lineIndex+1
      end
    end
  end
  

---
-- Loads exactly `CratesNeeded` crates for one cargoName in range.
-- If "Ammo Truck" needs 2 crates, we pick up 2 if available.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #string cargoName The cargo name, e.g. "Ammo Truck"
function CTLD:_LoadSingleCrateSet(Group, Unit, cargoName)
  self:T(self.lid .. " _LoadSingleCrateSet cargoName=" .. (cargoName or "nil"))

  -- 1) Must be landed or hovering
  local grounded = not self:IsUnitInAir(Unit)
  local hover    = self:CanHoverLoad(Unit)
  if not grounded and not hover then
    self:_SendMessage("You must land or hover to load crates!", 10, false, Group)
    return self
  end

  -- 2) Check door if required
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to load cargo!", 10, false, Group)
    return self
  end

  -- 3) Find crates with `cargoName` in range
  local finddist = self.CrateDistance or 35
  local cratesNearby, number = self:_FindCratesNearby(Group, Unit, finddist, false, false)
  if number == 0 then
    self:_SendMessage("No crates found in range!", 10, false, Group)
    return self
  end

  local matchingCrates = {}
  local needed = nil
  for _, crateObj in pairs(cratesNearby) do
    if crateObj:GetName() == cargoName then
      needed = needed or crateObj:GetCratesNeeded()
      table.insert(matchingCrates, crateObj)
    end
  end
  if not needed then
    self:_SendMessage(string.format("No \"%s\" crates found in range!", cargoName), 10, false, Group)
    return self
  end

  local found = #matchingCrates

  -- 4) Check capacity
  local unitName = Unit:GetName()
  local loadedData = self.Loaded_Cargo[unitName] or { Troopsloaded=0, Cratesloaded=0, Cargo={} }
  local capabilities = self:_GetUnitCapabilities(Unit)
  local capacity = capabilities.cratelimit or 0
  if loadedData.Cratesloaded >= capacity then
    self:_SendMessage("No more capacity to load crates!", 10, false, Group)
    return self
  end

  -- decide how many we can actually load
  local spaceLeft = capacity - loadedData.Cratesloaded
  local toLoad = math.min(found, needed, spaceLeft)
  if toLoad < 1 then
    self:_SendMessage("Cannot load crates: either none found or no capacity left.", 10, false, Group)
    return self
  end

  -- 5) Load exactly `toLoad` crates
  local crateIDsLoaded = {}
  for i = 1, toLoad do
    local crate = matchingCrates[i]
    crate:SetHasMoved(true)
    crate:SetWasDropped(false)
    table.insert(loadedData.Cargo, crate)
    loadedData.Cratesloaded = loadedData.Cratesloaded + 1
    local stObj = crate:GetPositionable()
    if stObj and stObj:IsAlive() then
      stObj:Destroy(false)
    end
    table.insert(crateIDsLoaded, crate:GetID())
  end
  self.Loaded_Cargo[unitName] = loadedData
  self:_UpdateUnitCargoMass(Unit)

  -- 6) Remove them from self.Spawned_Cargo
  local newSpawned = {}
  for _, cObj in ipairs(self.Spawned_Cargo) do
    local keep = true
    for i=1, toLoad do
      if matchingCrates[i] and cObj:GetID() == matchingCrates[i]:GetID() then
        keep = false
        break
      end
    end
    if keep then
      table.insert(newSpawned, cObj)
    end
  end
  self.Spawned_Cargo = newSpawned

  -- 7) Show final message, including a special note if capacity is now reached
  local loadedHere = toLoad
  if loadedHere < needed and loadedData.Cratesloaded >= capacity then
    self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s. Cargo limit is now reached!", loadedHere, needed, cargoName), 10, false, Group)
  else
    local fullSets = math.floor(loadedHere / needed)
    local leftover = loadedHere % needed
    if needed > 1 then
      if fullSets > 0 and leftover == 0 then
        self:_SendMessage(string.format("Loaded %d %s.", fullSets, cargoName), 10, false, Group)
      elseif fullSets > 0 and leftover > 0 then
        self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).", fullSets, cargoName, leftover), 10, false, Group)
      else
        self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.", loadedHere, needed, cargoName), 15, false, Group)
      end
    else
      self:_SendMessage(string.format("Loaded %d %s(s).", loadedHere, cargoName), 10, false, Group)
    end
  end

  self:_RefreshLoadCratesMenu(Group, Unit)
  self:_RefreshDropCratesMenu(Group, Unit)
  return self
end


--- (Internal) Function to unload a single crate
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @param #string setIndex The name of the crate to unload
-- @return #CTLD self
function CTLD:_UnloadSingleCrateSet(Group, Unit, setIndex)
  self:T(self.lid .. " _UnloadSingleCrateSet")

  -- Check if we are in a drop zone (unless we drop anywhere)
  if not self.dropcratesanywhere then
    local inzone, zoneName, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.DROP)
    if not inzone then
      self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group)
      if not self.debug then 
        return self 
      end
    end
  end

  -- Check if doors must be open
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to drop cargo!", 10, false, Group)
    if not self.debug then return self end
  end

  -- Check if the crate grouping data is available
  local unitName = Unit:GetName()
  if not self.CrateGroupList or not self.CrateGroupList[unitName] then
    self:_SendMessage("No crate groups found for this unit!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Find the selected chunk/set by index
  local chunk = self.CrateGroupList[unitName][setIndex]
  if not chunk then
    self:_SendMessage("No crate set found or index invalid!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Check if the chunk is empty
  if #chunk == 0 then
    self:_SendMessage("No crate found in that set!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Check hover/airdrop/landed logic
  local grounded = not self:IsUnitInAir(Unit)
  local hoverunload = self:IsCorrectHover(Unit)
  local isHerc = self:IsFixedWing(Unit)
  local isHook = self:IsHook(Unit)
  if isHerc and not isHook then
    hoverunload = self:IsCorrectFlightParameters(Unit)
  end
  if not grounded and not hoverunload then
    if isHerc then
      self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group)
    else
      self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group)
    end
    if not self.debug then return self end
    return self
  end

  -- Get the first crate from this set
  local crateObj = chunk[1]
  if not crateObj then
    self:_SendMessage("No crate found in that set!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Perform the actual "drop" spawn
  local needed = crateObj:GetCratesNeeded() or 1
  self:_GetCrates(Group, Unit, crateObj, #chunk, true)

  -- Mark all crates in the chunk as dropped
  for _, cObj in ipairs(chunk) do
    cObj:SetWasDropped(true)
    cObj:SetHasMoved(true)
  end
local cname  = crateObj:GetName() or "Unknown"
local count  = #chunk
if needed > 1 then
if count == needed then
    self:_SendMessage(string.format("Dropped %d %s.", 1, cname), 10, false, Group)
else
    self:_SendMessage(string.format("Dropped %d/%d crate(s) of %s.", count, needed, cname), 15, false, Group)
end
else
self:_SendMessage(string.format("Dropped %d %s(s).", count, cname), 10, false, Group)
end
  -- Rebuild the cargo list to remove the dropped crates
  local loadedData = self.Loaded_Cargo[unitName]
  if loadedData and loadedData.Cargo then
    local newList = {}
    local newCratesCount = 0
    for _, cObj in ipairs(loadedData.Cargo) do
      if not cObj:WasDropped() then
        table.insert(newList, cObj)
        local ct = cObj:GetType()
        if ct ~= CTLD_CARGO.Enum.TROOPS and ct ~= CTLD_CARGO.Enum.ENGINEERS then
          newCratesCount = newCratesCount + 1
        end
      end
    end
    loadedData.Cargo = newList
    loadedData.Cratesloaded = newCratesCount
    self.Loaded_Cargo[unitName] = loadedData
  end

  -- Update cargo mass, refresh menu
  self:_UpdateUnitCargoMass(Unit)
  self:_RefreshDropCratesMenu(Group, Unit)
  self:_RefreshLoadCratesMenu(Group, Unit)
  return self
end

--- (Internal) Function to refresh the menu for a single unit after crates dropped.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @return #CTLD self
function CTLD:_RefreshDropCratesMenu(Group, Unit)

    if not Group.CTLDTopmenu then return end
    local topCrates = Group.MyTopCratesMenu
    if not topCrates then return end
    if topCrates.DropCratesMenu then
      topCrates.DropCratesMenu:RemoveSubMenus()
    else
      topCrates.DropCratesMenu = MENU_GROUP:New(Group, "Drop Crates", topCrates)
    end
  
    local dropCratesMenu = topCrates.DropCratesMenu
    local loadedData = self.Loaded_Cargo[Unit:GetName()]
    if not loadedData or not loadedData.Cargo then
      MENU_GROUP_COMMAND:New(Group,"No crates to drop!",dropCratesMenu,function() end)
      return
    end
  
    local cargoByName={}
    local dropableCrates=0
    for _,cObj in ipairs(loadedData.Cargo) do
      if cObj and not cObj:WasDropped() then
        local cType=cObj:GetType()
        if cType~=CTLD_CARGO.Enum.TROOPS and cType~=CTLD_CARGO.Enum.ENGINEERS and cType~=CTLD_CARGO.Enum.GCLOADABLE then
          local name=cObj:GetName()or"Unknown"
          cargoByName[name]=cargoByName[name]or{}
          table.insert(cargoByName[name],cObj)
          dropableCrates=dropableCrates+1
        end
      end
    end
  
    if dropableCrates==0 then
      MENU_GROUP_COMMAND:New(Group,"No crates to drop!",dropCratesMenu,function() end)
      return
    end
  
    ----------------------------------------------------------------------
    -- DEFAULT (“classic”) versus ONE-STEP behaviour
    ----------------------------------------------------------------------
    if not self.onestepmenu then
      --------------------------------------------------------------------
      -- classic menu
      --------------------------------------------------------------------
      MENU_GROUP_COMMAND:New(Group,"Drop ALL crates",dropCratesMenu,self._UnloadCrates,self,Group,Unit)
  
      self.CrateGroupList=self.CrateGroupList or{}
      self.CrateGroupList[Unit:GetName()]={}
  
      local lineIndex=1
      for cName,list in pairs(cargoByName) do
        local needed=list[1]:GetCratesNeeded() or 1
        table.sort(list,function(a,b)return a:GetID()<b:GetID()end)
        local i=1
        while i<=#list do
          local left=(#list-i+1)
          if left>=needed then
            local chunk={}
            for n=i,i+needed-1 do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s",lineIndex,cName)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            i=i+needed
          else
            local chunk={}
            for n=i,#list do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            i=#list+1
          end
          lineIndex=lineIndex+1
        end
      end
  
    else
      --------------------------------------------------------------------
      -- one-step (enhanced) menu
      --------------------------------------------------------------------
      local mAll=MENU_GROUP:New(Group,"Drop ALL crates",dropCratesMenu)
      MENU_GROUP_COMMAND:New(Group,"Drop",mAll,self._UnloadCrates,self,Group,Unit)
      if not ( self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) ) then
        MENU_GROUP_COMMAND:New(Group,"Drop and build",mAll,self._DropAndBuild,self,Group,Unit)
      end

      self.CrateGroupList=self.CrateGroupList or{}
      self.CrateGroupList[Unit:GetName()]={}
  
      local lineIndex=1
      for cName,list in pairs(cargoByName) do
        local needed=list[1]:GetCratesNeeded() or 1
        table.sort(list,function(a,b)return a:GetID()<b:GetID()end)
        local i=1
        while i<=#list do
          local left=(#list-i+1)
          if left>=needed then
            local chunk={}
            for n=i,i+needed-1 do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s",lineIndex,cName)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            local mSet=MENU_GROUP:New(Group,label,dropCratesMenu)
            MENU_GROUP_COMMAND:New(Group,"Drop",mSet,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            if not ( self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) ) then
            MENU_GROUP_COMMAND:New(Group,"Drop and build",mSet,self._DropSingleAndBuild,self,Group,Unit,setIndex)
            end
            i=i+needed
          else
            local chunk={}
            for n=i,#list do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            i=#list+1
          end
          lineIndex=lineIndex+1
        end
      end
    end
  end

--- (Internal) Function to unload a single Troop group by ID.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @param #number chunkID the Cargo ID
-- @return #CTLD self
function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID)
  self:T(self.lid .. " _UnloadSingleTroopByID chunkID=" .. tostring(chunkID))

  local droppingatbase = false
  local inzone, zonename, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD)
  if not inzone then
    inzone, zonename, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP)
  end
  if inzone then
    droppingatbase = self.returntroopstobase
  end

  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group)
    if not self.debug then return self end 
  end

  local hoverunload = self:IsCorrectHover(Unit)
  local isHerc = self:IsFixedWing(Unit)
  local isHook = self:IsHook(Unit)
  if isHerc and not isHook then
    hoverunload = self:IsCorrectFlightParameters(Unit)
  end
  local grounded = not self:IsUnitInAir(Unit)
  local unitName = Unit:GetName()

  if self.Loaded_Cargo[unitName] and (grounded or hoverunload) then
    if not droppingatbase or self.debug then
      if not self.TroopsIDToChunk or not self.TroopsIDToChunk[chunkID] then
        self:_SendMessage(string.format("No troop cargo chunk found for ID %d!", chunkID), 10, false, Group)
        if not self.debug then return self end
        return self
      end

      local chunk = self.TroopsIDToChunk[chunkID]
      if not chunk or #chunk == 0 then
        self:_SendMessage(string.format("Troop chunk is empty for ID %d!", chunkID), 10, false, Group)
        if not self.debug then return self end
        return self
      end

      -- Drop the FIRST cargo in that chunk
      local foundCargo = chunk[1]
      if not foundCargo then
        self:_SendMessage(string.format("No troop cargo at chunk %d!", chunkID), 10, false, Group)
        if not self.debug then return self end
        return self
      end

      local cType = foundCargo:GetType()
      local name  = foundCargo:GetName() or "none"
      local tmpl  = foundCargo:GetTemplates() or {}
      local zoneradius = self.troopdropzoneradius or 100
      local factor = 1
      if isHerc then
        factor = foundCargo:GetCratesNeeded() or 1
        zoneradius = Unit:GetVelocityMPS() or 100
      end
      local zone = ZONE_GROUP:New(string.format("Unload zone-%s", unitName), Group, zoneradius * factor)
      local randomcoord = zone:GetRandomCoordinate(10, 30 * factor)
      local heading = Group:GetHeading() or 0

      if grounded or hoverunload then
        randomcoord = Group:GetCoordinate()
        local Angle = (heading + 270) % 360
        if isHerc or isHook then
          Angle = (heading + 180) % 360
        end
        local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround
        if isHerc then
          offset = self.TroopUnloadDistGroundHerc or 25
        end
        if isHook then
          offset = self.TroopUnloadDistGroundHook or 15
          if hoverunload and self.TroopUnloadDistHoverHook then
            offset = self.TroopUnloadDistHoverHook or 5
          end
        end
        randomcoord:Translate(offset, Angle, nil, true)
      end

      local tempcount = 0
      if isHook then
        tempcount = self.ChinookTroopCircleRadius or 5
      end
      for _, _template in pairs(tmpl) do
        self.TroopCounter = self.TroopCounter + 1
        tempcount = tempcount + 1
        local alias = string.format("%s-%d", _template, math.random(1,100000))
        local rad   = 2.5 + (tempcount * 2)
        local Positions = self:_GetUnitPositions(randomcoord, rad, heading, _template)
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias)
          :InitDelayOff()
          :InitSetUnitAbsolutePositions(Positions)
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
          :SpawnFromVec2(randomcoord:GetVec2())
        self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter], cType)
      end
      
      foundCargo:SetWasDropped(true)
      if cType == CTLD_CARGO.Enum.ENGINEERS then
        self.Engineers = self.Engineers + 1
          local grpname = self.DroppedTroops[self.TroopCounter]:GetName()
        self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname)
        self:_SendMessage(string.format("Dropped Engineers %s into action!", name), 10, false, Group)
      else
        self:_SendMessage(string.format("Dropped Troops %s into action!", name), 10, false, Group)
      end

      table.remove(chunk, 1)
      if #chunk == 0 then
        self.TroopsIDToChunk[chunkID] = nil
      end

    else
      -- Return to base logic, remove ONLY the first cargo
      self:_SendMessage("Troops have returned to base!", 10, false, Group)
      self:__TroopsRTB(1, Group, Unit, zonename, zone)

      if self.TroopsIDToChunk and self.TroopsIDToChunk[chunkID] then
        local chunk = self.TroopsIDToChunk[chunkID]
        if #chunk > 0 then
          local firstObj = chunk[1]
          local cName = firstObj:GetName()
          local gentroops = self.Cargo_Troops
          for _id, _troop in pairs(gentroops) do
            if _troop.Name == cName then
              local st = _troop:GetStock()
              if st and tonumber(st) >= 0 then
                _troop:AddStock()
              end
            end
          end
          firstObj:SetWasDropped(true)
          table.remove(chunk, 1)
          if #chunk == 0 then
            self.TroopsIDToChunk[chunkID] = nil
          end
        end
      end
    end

    local cargoList = self.Loaded_Cargo[unitName].Cargo
    for i = #cargoList, 1, -1 do
      if cargoList[i]:WasDropped() then
        table.remove(cargoList, i)
      end
    end
    local troopsLoaded = 0
    local cratesLoaded = 0
    for _, cargo in ipairs(cargoList) do
      local cT = cargo:GetType()
      if cT == CTLD_CARGO.Enum.TROOPS or cT == CTLD_CARGO.Enum.ENGINEERS then
        troopsLoaded = troopsLoaded + 1
      else
        cratesLoaded = cratesLoaded + 1
      end
    end
    self.Loaded_Cargo[unitName].Troopsloaded = troopsLoaded
    self.Loaded_Cargo[unitName].Cratesloaded = cratesLoaded
    self:_RefreshDropTroopsMenu(Group, Unit)
  else
    local isHerc = self:IsFixedWing(Unit)
    if isHerc then
      self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group)
    else
      self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group)
    end
  end
  return self
end

--- (Internal) Function to refresh menu for troops on drop for a specific unit
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The requesting group.
-- @param Wrapper.Unit#UNIT Unit The requesting unit.
-- @return #CTLD self
function CTLD:_RefreshDropTroopsMenu(Group, Unit)
  local theGroup = Group
  local theUnit  = Unit
  if not theGroup.CTLDTopmenu then return end
  local topTroops = theGroup.MyTopTroopsMenu
  if not topTroops then return end
  if topTroops.DropTroopsMenu then
    topTroops.DropTroopsMenu:Remove()
  end
  local dropTroopsMenu = MENU_GROUP:New(theGroup, "Drop Troops", topTroops)
  topTroops.DropTroopsMenu = dropTroopsMenu
  MENU_GROUP_COMMAND:New(theGroup, "Drop ALL troops", dropTroopsMenu, self._UnloadTroops, self, theGroup, theUnit)

  local loadedData = self.Loaded_Cargo[theUnit:GetName()]
  if not loadedData or not loadedData.Cargo then return end

  -- Gather troop cargo by name
  local troopsByName = {}
  for _, cargoObj in ipairs(loadedData.Cargo) do
    if cargoObj
       and (cargoObj:GetType() == CTLD_CARGO.Enum.TROOPS or cargoObj:GetType() == CTLD_CARGO.Enum.ENGINEERS)
       and not cargoObj:WasDropped()
    then
      local name = cargoObj:GetName() or "Unknown"
      troopsByName[name] = troopsByName[name] or {}
      table.insert(troopsByName[name], cargoObj)
    end
  end

  self.TroopsIDToChunk = self.TroopsIDToChunk or {}

  for tName, objList in pairs(troopsByName) do
    table.sort(objList, function(a,b) return a:GetID() < b:GetID() end)
    local count = #objList

    local chunkID = objList[1]:GetID()
    self.TroopsIDToChunk[chunkID] = objList

    local label = string.format("Drop %s (%d)", tName, count)
    MENU_GROUP_COMMAND:New(theGroup, label, dropTroopsMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID)
  end
end

--- [Internal] Function to check if a template exists in the mission.
-- @param #CTLD self
-- @param #table temptable Table of string names
-- @return #boolean outcome
function CTLD:_CheckTemplates(temptable)
  self:T(self.lid .. " _CheckTemplates")
  local outcome = true
  if type(temptable) ~= "table" then
    temptable = {temptable}
  end
  for _,_name in pairs(temptable) do
    if not _DATABASE.Templates.Groups[_name] then
      outcome = false
      self:E(self.lid .. "ERROR: Template name " .. _name ..  " is missing!")
    end
  end
  return outcome
end

--- User function - Add *generic* troop type loadable as cargo. This type will load directly into the heli without crates.
-- @param #CTLD self
-- @param #string Name Unique name of this type of troop. E.g. "Anti-Air Small".
-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP making up this troop.
-- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build.
-- @param #number NoTroops Size of the group in number of Units across combined templates (for loading).
-- @param #number PerTroopMass Mass in kg of each soldier
-- @param #number Stock Number of groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of sub-category (optional).
function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,SubCategory) 
  self:T(self.lid .. " AddTroopsCargo")
  self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock})
  if not self:_CheckTemplates(Templates) then
    self:E(self.lid .. "Troops Cargo for " .. Name .. " has missing template(s)!" )
    return self
  end
  self.CargoCounter = self.CargoCounter + 1
  -- Troops are directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory)
  table.insert(self.Cargo_Troops,cargo)
  if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
  return self
end

--- User function - Add *generic* crate-type loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built.
-- @param #CTLD self
-- @param #string Name Unique name of this type of cargo. E.g. "Humvee".
-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo.
-- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put.
-- @param #number NoCrates Number of crates needed to build this cargo.
-- @param #number PerCrateMass Mass in kg of each crate
-- @param #number Stock Number of buildable groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu.
-- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string.
-- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}.
-- @param #string Category Static category name (optional). If set, spawn cargo crate with an alternate category type, e.g. "Cargos".
-- @param #string TypeName Static type name (optional). If set, spawn cargo crate with an alternate type shape, e.g. "iso_container".
-- @param #string ShapeName Static shape name (optional). If set, spawn cargo crate with an alternate type sub-shape, e.g. "iso_container_cargo".
-- @return #CTLD self
function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName)
  self:T(self.lid .. " AddCratesCargo")
  if not self:_CheckTemplates(Templates) then
    self:E(self.lid .. "Crates Cargo for " .. Name .. " has missing template(s)!" )
    return self
  end
  self.CargoCounter = self.CargoCounter + 1
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location)
  if UnitTypes then
    cargo:AddUnitTypeName(UnitTypes)
  end
  cargo:SetStaticTypeAndShape("Cargos",self.basetype)
  if TypeName then
    cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName)
  end
  table.insert(self.Cargo_Crates,cargo)
  if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
  return self
end

--- User function - Add *generic* static-type loadable as cargo. This type will create cargo that needs to be loaded, moved and dropped.
-- @param #CTLD self
-- @param #string Name Unique name of this type of cargo as set in the mission editor (note: UNIT name!), e.g. "Ammunition-1".
-- @param #number Mass Mass in kg of each static in kg, e.g. 100.
-- @param #number Stock Number of groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu.
-- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string.
-- @return #CTLD_CARGO CargoObject
function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location)
  self:T(self.lid .. " AddStaticsCargo")
  self.CargoCounter = self.CargoCounter + 1
  local type = CTLD_CARGO.Enum.STATIC
  local template = STATIC:FindByName(Name,true):GetTypeName()
  local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
  local ResourceMap = nil
  if unittemplate and unittemplate.resourcePayload then
    ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
  end
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location)
  cargo:SetStaticResourceMap(ResourceMap)
  table.insert(self.Cargo_Statics,cargo)
  if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
  return cargo
end

--- User function - Get a *generic* static-type loadable as #CTLD_CARGO object.
-- @param #CTLD self
-- @param #string Name Unique Unit(!) name of this type of cargo as set in the mission editor (not: GROUP name!), e.g. "Ammunition-1".
-- @param #number Mass Mass in kg of each static in kg, e.g. 100.
-- @return #CTLD_CARGO Cargo object
function CTLD:GetStaticsCargoFromTemplate(Name,Mass)
  self:T(self.lid .. " GetStaticsCargoFromTemplate")
  self.CargoCounter = self.CargoCounter + 1
  local type = CTLD_CARGO.Enum.STATIC
  local template = STATIC:FindByName(Name,true):GetTypeName()
  local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
  local ResourceMap = nil
  if unittemplate and unittemplate.resourcePayload then
    ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
  end
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1)
  cargo:SetStaticResourceMap(ResourceMap)
  --table.insert(self.Cargo_Statics,cargo)
  return cargo
end

--- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built.
-- @param #CTLD self
-- @param #string Name Unique name of this type of cargo. E.g. "Humvee".
-- @param #string Template Template of VEHICLE or FOB cargo that this can repair. MUST be the same as given in `AddCratesCargo(..)`!
-- @param #CTLD_CARGO.Enum Type Type of cargo, here REPAIR.
-- @param #number NoCrates Number of crates needed to build this cargo.
-- @param #number PerCrateMass Mass in kg of each crate
-- @param #number Stock Number of groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of the sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu.
-- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string.
-- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}
-- @param #string Category Static category name (optional). If set, spawn cargo crate with an alternate category type, e.g. "Cargos".
-- @param #string TypeName Static type name (optional). If set, spawn cargo crate with an alternate type shape, e.g. "iso_container".
-- @param #string ShapeName Static shape name (optional). If set, spawn cargo crate with an alternate type sub-shape, e.g. "iso_container_cargo".
-- @return #CTLD self
function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName)
  self:T(self.lid .. " AddCratesRepair")
  if not self:_CheckTemplates(Template) then
    self:E(self.lid .. "Repair Cargo for " .. Name .. " has a missing template!" )
    return self
  end
  self.CargoCounter = self.CargoCounter + 1
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location)
  if UnitTypes then
    cargo:AddUnitTypeName(UnitTypes)
  end
  cargo:SetStaticTypeAndShape("cargos",self.basetype)
  if TypeName then
    cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName)
  end
  table.insert(self.Cargo_Crates,cargo)
  return self
end

--- User function - Add a #CTLD.CargoZoneType zone for this CTLD instance.
-- @param #CTLD self
-- @param #CTLD.CargoZone Zone Zone #CTLD.CargoZone describing the zone.
function CTLD:AddZone(Zone)
  self:T(self.lid .. " AddZone")
  local zone = Zone -- #CTLD.CargoZone
  if zone.type == CTLD.CargoZoneType.LOAD then
    table.insert(self.pickupZones,zone)
  elseif zone.type == CTLD.CargoZoneType.DROP then
    table.insert(self.dropOffZones,zone)
  elseif zone.type == CTLD.CargoZoneType.SHIP then
    table.insert(self.shipZones,zone)
  elseif zone.type == CTLD.CargoZoneType.BEACON then
    table.insert(self.droppedBeacons,zone)   
  else
    table.insert(self.wpZones,zone)
  end
  return self
end

--- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance.
-- @param #CTLD self
-- @param #string Name Name of the zone to change in the ME.
-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to.
-- @param #boolean NewState (Optional) Set to true to activate, false to switch off.
function CTLD:ActivateZone(Name,ZoneType,NewState)
  self:T(self.lid .. " ActivateZone")
  local newstate = true
  -- set optional in case we\'re deactivating
  if NewState ~= nil then
    newstate = NewState
  end  
  
  -- get correct table
  local table = {}
  if ZoneType == CTLD.CargoZoneType.LOAD then
    table = self.pickupZones
  elseif ZoneType == CTLD.CargoZoneType.DROP then
    table = self.dropOffZones
  elseif ZoneType == CTLD.CargoZoneType.SHIP then
    table = self.shipZones
  elseif ZoneType == CTLD.CargoZoneType.BEACON then
    table = self.droppedBeacons
  else
    table = self.wpZones
  end
  -- loop table
  for _,_zone in pairs(table) do
    local thiszone = _zone --#CTLD.CargoZone
    if thiszone.name == Name then
      thiszone.active = newstate
      break
    end
  end
  return self
end


--- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance.
-- @param #CTLD self
-- @param #string Name Name of the zone to change in the ME.
-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to.
function CTLD:DeactivateZone(Name,ZoneType)
  self:T(self.lid .. " DeactivateZone")
  self:ActivateZone(Name,ZoneType,false)
  return self
end

--- (Internal) Function to obtain a valid FM frequency.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @return #CTLD.ZoneBeacon Beacon Beacon table.
function CTLD:_GetFMBeacon(Name)
  self:T(self.lid .. " _GetFMBeacon")
  local beacon = {} -- #CTLD.ZoneBeacon  
  if #self.FreeFMFrequencies <= 1 then
      self.FreeFMFrequencies = self.UsedFMFrequencies
      self.UsedFMFrequencies = {}
  end 
  --random
  local FM = table.remove(self.FreeFMFrequencies, math.random(#self.FreeFMFrequencies))
  table.insert(self.UsedFMFrequencies, FM)  
  beacon.name = Name
  beacon.frequency = FM / 1000000
  beacon.modulation = CTLD.RadioModulation.FM
  return beacon
end

--- (Internal) Function to obtain a valid UHF frequency.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @return #CTLD.ZoneBeacon Beacon Beacon table.
function CTLD:_GetUHFBeacon(Name)
  self:T(self.lid .. " _GetUHFBeacon")
  local beacon = {} -- #CTLD.ZoneBeacon  
  if #self.FreeUHFFrequencies <= 1 then
      self.FreeUHFFrequencies = self.UsedUHFFrequencies
      self.UsedUHFFrequencies = {}
  end 
  --random
  local UHF = table.remove(self.FreeUHFFrequencies, math.random(#self.FreeUHFFrequencies))
  table.insert(self.UsedUHFFrequencies, UHF)
  beacon.name = Name
  beacon.frequency = UHF / 1000000
  beacon.modulation = CTLD.RadioModulation.AM

  return beacon
end

--- (Internal) Function to obtain a valid VHF frequency.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @return #CTLD.ZoneBeacon Beacon Beacon table.
function CTLD:_GetVHFBeacon(Name)
  self:T(self.lid .. " _GetVHFBeacon")
  local beacon = {} -- #CTLD.ZoneBeacon
  if #self.FreeVHFFrequencies <= 3 then
      self.FreeVHFFrequencies = self.UsedVHFFrequencies
      self.UsedVHFFrequencies = {}
  end
  --get random
  local VHF = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies))
  table.insert(self.UsedVHFFrequencies, VHF)
  beacon.name = Name
  beacon.frequency = VHF / 1000000
  beacon.modulation = CTLD.RadioModulation.FM
  return beacon
end


--- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance.
--  Zones of type LOAD: Players load crates and troops here.  
--  Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere.  
--  Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options).  
-- @param #CTLD self
-- @param #string Name Name of this zone, as in Mission Editor.
-- @param #string Type Type of this zone, #CTLD.CargoZoneType
-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red
-- @param #string Active Is this zone currently active?
-- @param #string HasBeacon Does this zone have a beacon if it is active?
-- @param #number Shiplength Length of Ship for shipzones
-- @param #number Shipwidth Width of Ship for shipzones
-- @return #CTLD self
function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Shipwidth)
  self:T(self.lid .. " AddCTLDZone")
  
  local zone = ZONE:FindByName(Name)
  if not zone and  Type ~= CTLD.CargoZoneType.SHIP then
    self:E(self.lid.."**** Zone does not exist: "..Name)
    return self
  end
  
  if Type == CTLD.CargoZoneType.SHIP then
  local Ship = UNIT:FindByName(Name)
  if not Ship then
      self:E(self.lid.."**** Ship does not exist: "..Name)
    return self
  end
  end

  local exists = true
  local ctldzone = self:GetCTLDZone(Name, Type) -- #CTLD.CargoZone
  if not ctldzone then
    exists = false
    ctldzone = {}
  end

  ctldzone.active = Active or false
  ctldzone.color = Color or SMOKECOLOR.Red
  ctldzone.name = Name or "NONE"
  ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType
  ctldzone.hasbeacon = HasBeacon or false
  
  if Type == CTLD.CargoZoneType.BEACON then
    self.droppedbeaconref[ctldzone.name] = zone:GetCoordinate()
    ctldzone.timestamp = timer.getTime()
  end
     
  if HasBeacon then
    ctldzone.fmbeacon = self:_GetFMBeacon(Name)
    ctldzone.uhfbeacon = self:_GetUHFBeacon(Name)
    ctldzone.vhfbeacon = self:_GetVHFBeacon(Name)
  else
    ctldzone.fmbeacon = nil
    ctldzone.uhfbeacon = nil
    ctldzone.vhfbeacon = nil
  end
  
  if Type == CTLD.CargoZoneType.SHIP then
   ctldzone.shiplength = Shiplength or 100
   ctldzone.shipwidth = Shipwidth or 10
  end

  if not exists then
    self:AddZone(ctldzone)
  end
  return self
end


--- User function - find #CTLD.CargoZone zone by name.
-- @param #CTLD self
-- @param #string Name Name of this zone.
-- @param #string Type Type of this zone, #CTLD.CargoZoneType
-- @return #CTLD.CargoZone self
function CTLD:GetCTLDZone(Name, Type)

  if Type == CTLD.CargoZoneType.LOAD then
    for _, z in pairs(self.pickupZones) do
        if z.name == Name then
            return z
        end
    end
  elseif Type == CTLD.CargoZoneType.DROP then
    for _, z in pairs(self.dropOffZones) do
        if z.name == Name then
            return z
        end
    end
  elseif Type == CTLD.CargoZoneType.SHIP then
    for _, z in pairs(self.shipZones) do
        if z.name == Name then
            return z
        end
    end
  elseif Type == CTLD.CargoZoneType.BEACON then
    for _, z in pairs(self.droppedBeacons) do
        if z.name == Name then
            return z
        end
    end
  else
    for _, z in pairs(self.wpZones) do
        if z.name == Name then
            return z
        end
    end
  end

  return nil
end

--- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance from an Airbase or FARP name.
--  Zones of type LOAD: Players load crates and troops here.  
--  Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere.  
--  Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options).  
-- @param #CTLD self
-- @param #string AirbaseName Name of the Airbase, can be e.g. AIRBASE.Caucasus.Beslan or "Beslan". For FARPs, this will be the UNIT name.
-- @param #string Type Type of this zone, #CTLD.CargoZoneType
-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red
-- @param #string Active Is this zone currently active?
-- @param #string HasBeacon Does this zone have a beacon if it is active?
-- @return #CTLD self
function CTLD:AddCTLDZoneFromAirbase(AirbaseName, Type, Color, Active, HasBeacon)
  self:T(self.lid .. " AddCTLDZoneFromAirbase")
  local AFB = AIRBASE:FindByName(AirbaseName)
  local name = AFB:GetZone():GetName()
  self:T(self.lid .. "AFB " .. AirbaseName .. " ZoneName " .. name)
  self:AddCTLDZone(name, Type, Color, Active, HasBeacon)
  return self
end

--- (Internal) Function to create a dropped beacon
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:DropBeaconNow(Unit)
  self:T(self.lid .. " DropBeaconNow")
  
  local ctldzone = {} -- #CTLD.CargoZone
  ctldzone.active = true
  ctldzone.color = math.random(0,4) -- random color
  ctldzone.name = "Beacon " .. math.random(1,10000)
  ctldzone.type = CTLD.CargoZoneType.BEACON -- #CTLD.CargoZoneType
  ctldzone.hasbeacon = true
   
  ctldzone.fmbeacon = self:_GetFMBeacon(ctldzone.name)
  ctldzone.uhfbeacon = self:_GetUHFBeacon(ctldzone.name)
  ctldzone.vhfbeacon = self:_GetVHFBeacon(ctldzone.name)
  ctldzone.timestamp = timer.getTime()
  
  self.droppedbeaconref[ctldzone.name] = Unit:GetCoordinate()
  
  self:AddZone(ctldzone)
  
  local FMbeacon = ctldzone.fmbeacon -- #CTLD.ZoneBeacon
  local VHFbeacon = ctldzone.vhfbeacon -- #CTLD.ZoneBeacon
  local UHFbeacon = ctldzone.uhfbeacon -- #CTLD.ZoneBeacon
  local Name = ctldzone.name
  local FM = FMbeacon.frequency  -- MHz
  local VHF = VHFbeacon.frequency * 1000 -- KHz
  local UHF = UHFbeacon.frequency  -- MHz
  local text = string.format("Dropped %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF)
  
  self:_SendMessage(text,15,false,Unit:GetGroup())
  
  return self
end

--- (Internal) Housekeeping dropped beacons.
-- @param #CTLD self
-- @return #CTLD self
function CTLD:CheckDroppedBeacons()
  self:T(self.lid .. " CheckDroppedBeacons")
  
  -- check for timeout
  local timeout = self.droppedbeacontimeout or 600
  local livebeacontable = {}
  
  for _,_beacon in pairs (self.droppedBeacons) do
    local beacon = _beacon -- #CTLD.CargoZone
    if not beacon.timestamp then beacon.timestamp = timer.getTime() + timeout end
    local T0 = beacon.timestamp 
    if timer.getTime() - T0 > timeout then
      local name = beacon.name
      self.droppedbeaconref[name] = nil
      _beacon = nil
    else
      table.insert(livebeacontable,beacon)
    end
  end
  
  self.droppedBeacons = nil
  self.droppedBeacons = livebeacontable
  
  return self
end

--- (Internal) Function to show list of radio beacons
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_ListRadioBeacons(Group, Unit)
  self:T(self.lid .. " _ListRadioBeacons")
  local report = REPORT:New("Active Zone Beacons")
  report:Add("------------------------------------------------------------")
  local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons}
  for i=1,5 do
    for index,cargozone in pairs(zones[i]) do
      -- Get Beacon object from zone
      local czone = cargozone -- #CTLD.CargoZone
      if czone.active and czone.hasbeacon then
        local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon
        local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon
        local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon
        local Name = czone.name
        local FM = FMbeacon.frequency  -- MHz
        local VHF = VHFbeacon.frequency * 1000 -- KHz
        local UHF = UHFbeacon.frequency  -- MHz
        report:AddIndent(string.format(" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF),"|")
      end
    end
  end
  if report:GetCount() == 1 then
    report:Add("        N O N E")
  end
  report:Add("------------------------------------------------------------")
  self:_SendMessage(report:Text(), 30, true, Group) 
  return self
end

--- (Internal) Add radio beacon to zone. Runs 30 secs.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @param #string Sound Name of soundfile.
-- @param #number Mhz Frequency in Mhz.
-- @param #number Modulation Modulation AM or FM.
-- @param #boolean IsShip If true zone is a ship.
-- @param #boolean IsDropped If true, this isn't a zone but a dropped beacon
function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip, IsDropped)
  self:T(self.lid .. " _AddRadioBeacon")
  local Zone = nil
  if IsShip then
    Zone = UNIT:FindByName(Name)
  elseif IsDropped then
    Zone = self.droppedbeaconref[Name]
  else
    Zone = ZONE:FindByName(Name)
    if not Zone then
      Zone = AIRBASE:FindByName(Name):GetZone()
    end
  end
  local Sound = Sound or "beacon.ogg"
  if Zone then
  if IsDropped then
    local ZoneCoord = Zone
    local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0}
    local Frequency = Mhz * 1000000 -- Freq in Hertz
    local Sound =  self.RadioPath..Sound
    trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straight
    self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = %d %d %d | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation))
  else
    local ZoneCoord = Zone:GetCoordinate()
    local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0}
    local Frequency = Mhz * 1000000 -- Freq in Hert
    local Sound =  self.RadioPath..Sound
    trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straightt
    self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = {x=%d, y=%d, z=%d} | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation))
    end
  else
  self:E(self.lid.."***** _AddRadioBeacon: Zone does not exist: "..Name)
  end
  return self
end

--- Set folder path where the CTLD sound files are located **within you mission (miz) file**.
-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission.
-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used.
-- @param #CTLD self
-- @param #string FolderPath The path to the sound files, e.g. "CTLD_Soundfiles/".
-- @return #CTLD self
function CTLD:SetSoundfilesFolder( FolderPath )
  self:T(self.lid .. " SetSoundfilesFolder")
  -- Check that it ends with /
  if FolderPath then
      local lastchar = string.sub( FolderPath, -1 )
      if lastchar ~= "/" then
          FolderPath = FolderPath .. "/"
      end
  end

  -- Folderpath.
  self.RadioPath = FolderPath

  -- Info message.
  self:I( self.lid .. string.format( "Setting sound files folder to: %s", self.RadioPath ) )

  return self
end

--- (Internal) Function to refresh radio beacons
-- @param #CTLD self
function CTLD:_RefreshRadioBeacons()
  self:T(self.lid .. " _RefreshRadioBeacons")

  local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons}
  for i=1,5 do
    local IsShip = false
    if i == 4 then IsShip = true end
    local IsDropped = false
    if i == 5 then IsDropped = true end
    for index,cargozone in pairs(zones[i]) do
      -- Get Beacon object from zone
      local czone = cargozone -- #CTLD.CargoZone
      local Sound = self.RadioSound
      local Silent = self.RadioSoundFC3 or self.RadioSound
      if czone.active and czone.hasbeacon then
        local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon
        local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon
        local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon
        local Name = czone.name
        local FM = FMbeacon.frequency  -- MHz
        local VHF = VHFbeacon.frequency -- KHz
        local UHF = UHFbeacon.frequency  -- MHz   
        self:_AddRadioBeacon(Name,Sound,FM, CTLD.RadioModulation.FM, IsShip, IsDropped)
        self:_AddRadioBeacon(Name,Sound,VHF,CTLD.RadioModulation.AM, IsShip, IsDropped)
        self:_AddRadioBeacon(Name,Silent,UHF,CTLD.RadioModulation.AM, IsShip, IsDropped)
      end
    end
  end
  return self
end

--- (Internal) Function to see if a unit is in a specific zone type.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit Unit
-- @param #CTLD.CargoZoneType Zonetype Zonetype
-- @return #boolean Outcome Is in zone or not
-- @return #string name Closest zone name
-- @return Core.Zone#ZONE zone Closest Core.Zone#ZONE object
-- @return #number distance Distance to closest zone
-- @return #number width Radius of zone or width of ship
function CTLD:IsUnitInZone(Unit,Zonetype)
  self:T(self.lid .. " IsUnitInZone")
  self:T(Zonetype)
  local unitname = Unit:GetName()
  local zonetable = {}
  local outcome = false
  if Zonetype == CTLD.CargoZoneType.LOAD then
    zonetable = self.pickupZones -- #table
  elseif Zonetype == CTLD.CargoZoneType.DROP then
    zonetable = self.dropOffZones -- #table
  elseif Zonetype == CTLD.CargoZoneType.SHIP then
    zonetable = self.shipZones -- #table
  else 
   zonetable = self.wpZones -- #table
  end
  --- now see if we\'re in
  local zonecoord = nil
  local colorret = nil
  local maxdist = 1000000 -- 100km
  local zoneret = nil
  local zonewret = nil
  local zonenameret = nil
  local unitcoord = Unit:GetCoordinate()
  local unitVec2 = unitcoord:GetVec2()
  for _,_cargozone in pairs(zonetable) do
    local czone = _cargozone -- #CTLD.CargoZone
    local zonename = czone.name
    local active = czone.active
    local color = czone.color
    local zone = nil
    local zoneradius = 100
    local zonewidth = 20
    if Zonetype == CTLD.CargoZoneType.SHIP then
      self:T("Checking Type Ship: "..zonename)
      local ZoneUNIT = UNIT:FindByName(zonename)
      if not ZoneUNIT then return false end
      zonecoord = ZoneUNIT:GetCoordinate()
      zoneradius = czone.shiplength
      zonewidth = czone.shipwidth
      zone = ZONE_UNIT:New( ZoneUNIT:GetName(), ZoneUNIT, zoneradius/2)
    elseif ZONE:FindByName(zonename) then
      zone = ZONE:FindByName(zonename)
      self:T("Checking Zone: "..zonename)
      zonecoord = zone:GetCoordinate()
      --zoneradius = 1500
      zonewidth = zoneradius
    elseif AIRBASE:FindByName(zonename) then
      zone = AIRBASE:FindByName(zonename):GetZone()
      self:T("Checking Zone: "..zonename)
      zonecoord = zone:GetCoordinate()
      zoneradius = 2000
      zonewidth = zoneradius
    end
    local distance = self:_GetDistance(zonecoord,unitcoord)
    self:T("Distance Zone: "..distance)
    self:T("Zone Active: "..tostring(active))
    if (zone:IsVec2InZone(unitVec2) or Zonetype == CTLD.CargoZoneType.MOVE) and active == true and distance < maxdist then 
      outcome = true
      maxdist = distance
      zoneret = zone 
      zonenameret = zonename
      zonewret = zonewidth
      colorret = color 
    end
  end
  if Zonetype == CTLD.CargoZoneType.SHIP then
    return outcome, zonenameret, zoneret, maxdist, zonewret
  else
    return outcome, zonenameret, zoneret, maxdist
  end
end

--- User function - Drop a smoke or flare at current location.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The Unit.
-- @param #boolean Flare If true, flare instead.
-- @param #number SmokeColor Color enumerator for smoke, e.g. SMOKECOLOR.Red
function CTLD:SmokePositionNow(Unit, Flare, SmokeColor)
  self:T(self.lid .. " SmokePositionNow")
  local Smokecolor = self.SmokeColor or SMOKECOLOR.Red
  if SmokeColor then 
    Smokecolor = SmokeColor
  end
  local FlareColor = self.FlareColor or FLARECOLOR.Red
  -- table of #CTLD.CargoZone table
  local unitcoord = Unit:GetCoordinate() -- Core.Point#COORDINATE
  local Group = Unit:GetGroup()
  if Flare then
    unitcoord:Flare(FlareColor, 90)
  else
    local height = unitcoord:GetLandHeight() + 2
    unitcoord.y = height
    unitcoord:Smoke(Smokecolor)
  end
  return self
end

--- User function - Start smoke/flare in a zone close to the Unit.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The Unit.
-- @param #boolean Flare If true, flare instead.
function CTLD:SmokeZoneNearBy(Unit, Flare)
  self:T(self.lid .. " SmokeZoneNearBy")
  -- table of #CTLD.CargoZone table
  local unitcoord = Unit:GetCoordinate()
  local Group = Unit:GetGroup()
  local smokedistance = self.smokedistance
  local smoked = false
  local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones}
  for i=1,4 do
    for index,cargozone in pairs(zones[i]) do
      local CZone = cargozone --#CTLD.CargoZone
      local zonename = CZone.name
      local zone = nil -- Core.Zone#ZONE_RADIUS
      local airbasezone = false
      if i == 4 then
        zone = UNIT:FindByName(zonename)
      else
        zone = ZONE:FindByName(zonename)
        if not zone then
          zone = AIRBASE:FindByName(zonename):GetZone()
          airbasezone = true
        end
      end
      local zonecoord = zone:GetCoordinate()
      -- Avoid smoke/flares on runways
      if (i==1 or 1==3) and airbasezone==true and zone:IsInstanceOf("ZONE_BASE") then
        zonecoord = zone:GetRandomCoordinate(inner,outer,{land.SurfaceType.LAND})
      end
    if zonecoord then
      local active = CZone.active
      local color = CZone.color
      local distance = self:_GetDistance(zonecoord,unitcoord)
      if distance < smokedistance and active then
        -- smoke zone since we\'re nearby
        if not Flare then 
          zonecoord:Smoke(color or SMOKECOLOR.White)
        else
          if color == SMOKECOLOR.Blue then color = FLARECOLOR.White end
          zonecoord:Flare(color or FLARECOLOR.White)
        end
        local txt = "smoking"
        if Flare then txt = "flaring" end
        self:_SendMessage(string.format("Roger, %s zone %s!",txt, zonename), 10, false, Group)
        smoked = true
      end
     end
    end
  end
  if not smoked then
    local distance = UTILS.MetersToNM(self.smokedistance)
    self:_SendMessage(string.format("Negative, need to be closer than %dnm to a zone!",distance), 10, false, Group)
  end
    return self 
end

  --- User - Function to add/adjust unittype capabilities.
  -- @param #CTLD self
  -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission.
  -- @param #boolean Cancrates Unit can load crates. Default false.
  -- @param #boolean Cantroops Unit can load troops. Default false.
  -- @param #number Cratelimit Unit can carry number of crates. Default 0.
  -- @param #number Trooplimit Unit can carry number of troops. Default 0.
  -- @param #number Length Unit lenght (in metres) for the load radius. Default 20.
  -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500.
  function CTLD:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight)
    self:T(self.lid .. " UnitCapabilities")
    local unittype =  nil
    local unit = nil
    if type(Unittype) == "string" then
      unittype = Unittype
    elseif type(Unittype) == "table" and Unittype.ClassName and Unittype:IsInstanceOf("UNIT") then
      unittype = Unittype:GetTypeName()
    else
      return self
    end
    local length = 20
    local maxcargo = 500
    local existingcaps = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities
    if existingcaps then
      length = existingcaps.length or 20
      maxcargo = existingcaps.cargoweightlimit or 500
    end
    -- set capabilities
    local capabilities = {} -- #CTLD.UnitTypeCapabilities
    capabilities.type = unittype
    capabilities.crates = Cancrates or false
    capabilities.troops = Cantroops or false
    capabilities.cratelimit = Cratelimit or  0
    capabilities.trooplimit = Trooplimit or 0
    capabilities.length = Length or length
    capabilities.cargoweightlimit = Maxcargoweight or maxcargo
    self.UnitTypeCapabilities[unittype] = capabilities
    return self
  end
  
  --- User - Function to add onw SET_GROUP Set-up for pilot filtering and assignment.
  -- Needs to be set before starting the CTLD instance.
  -- @param #CTLD self
  -- @param Core.Set#SET_GROUP Set The SET_GROUP object created by the mission designer/user to represent the CTLD pilot groups.
  -- @return #CTLD self 
  function CTLD:SetOwnSetPilotGroups(Set)
    self.UserSetGroup = Set
    return self
  end
  
  --- [Deprecated] - Function to add/adjust unittype capabilities. Has been replaced with `SetUnitCapabilities()` - pls use the new one going forward!
  -- @param #CTLD self
  -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission.
  -- @param #boolean Cancrates Unit can load crates. Default false.
  -- @param #boolean Cantroops Unit can load troops. Default false.
  -- @param #number Cratelimit Unit can carry number of crates. Default 0.
  -- @param #number Trooplimit Unit can carry number of troops. Default 0.
  -- @param #number Length Unit lenght (in metres) for the load radius. Default 20.
  -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500.
  function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight)
    self:I(self.lid.."This function been replaced with `SetUnitCapabilities()` - pls use the new one going forward!")
    self:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight)
    return self
  end
  
  
  --- (Internal) Check if a unit is hovering *in parameters*.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:IsCorrectHover(Unit)
    self:T(self.lid .. " IsCorrectHover")
    local outcome = false
    -- see if we are in air and within parameters.
    if self:IsUnitInAir(Unit) then
      -- get speed and height
      local uspeed = Unit:GetVelocityMPS()
      local uheight = Unit:GetHeight()
      local ucoord = Unit:GetCoordinate()
      if not ucoord then
        return false
      end
      local gheight = ucoord:GetLandHeight()
      local aheight = uheight - gheight -- height above ground
      local maxh = self.maximumHoverHeight -- 15
      local minh =  self.minimumHoverHeight -- 5
      local mspeed = 2 -- 2 m/s
      if (uspeed <= mspeed) and (aheight <= maxh) and (aheight >= minh)  then 
        -- yep within parameters
        outcome = true
      end
    end
    return outcome
  end
  
    --- (Internal) Check if a Hercules is flying *in parameters* for air drops.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:IsCorrectFlightParameters(Unit)
    self:T(self.lid .. " IsCorrectFlightParameters")
    local outcome = false
    -- see if we are in air and within parameters.
    if self:IsUnitInAir(Unit) then
      -- get speed and height
      local uspeed = Unit:GetVelocityMPS()
      local uheight = Unit:GetHeight()
      local ucoord = Unit:GetCoordinate()
      if not ucoord then
        return false
      end
      local gheight = ucoord:GetLandHeight()
      local aheight = uheight - gheight -- height above ground
      local minh = self.FixedMinAngels-- 1500m
      local maxh =  self.FixedMaxAngels -- 5000m
      local maxspeed =  self.FixedMaxSpeed -- 77 mps
      -- DONE: TEST - Speed test for Herc, should not be above 280kph/150kn
      local kmspeed = uspeed * 3.6
      local knspeed = kmspeed / 1.86
      self:T(string.format("%s Unit parameters: at %dm AGL with %dmps | %dkph | %dkn",self.lid,aheight,uspeed,kmspeed,knspeed))
      if (aheight <= maxh) and (aheight >= minh) and (uspeed <= maxspeed) then 
        -- yep within parameters
        outcome = true
      end
    end
    return outcome
  end
  
  --- (Internal) List if a unit is hovering *in parameters*.
  -- @param #CTLD self
  -- @param Wrapper.Group#GROUP Group
  -- @param Wrapper.Unit#UNIT Unit
  function CTLD:_ShowHoverParams(Group,Unit)
    local inhover = self:IsCorrectHover(Unit)
    local htxt = "true"
    if not inhover then htxt = "false" end
    local text = ""
    if _SETTINGS:IsMetric() then
      text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", self.minimumHoverHeight, self.maximumHoverHeight, htxt)
    else
      local minheight = UTILS.MetersToFeet(self.minimumHoverHeight)
      local maxheight = UTILS.MetersToFeet(self.maximumHoverHeight)
      text = string.format("Hover parameters (autoload/drop):\n - Min height %dft \n - Max height %dft \n - Max speed 6ftps \n - In parameter: %s", minheight, maxheight, htxt)
    end
    self:_SendMessage(text, 10, false, Group)
    return self
  end
  
    --- (Internal) List if a Herc unit is flying *in parameters*.
  -- @param #CTLD self
  -- @param Wrapper.Group#GROUP Group
  -- @param Wrapper.Unit#UNIT Unit
  function CTLD:_ShowFlightParams(Group,Unit)
    local inhover = self:IsCorrectFlightParameters(Unit)
    local htxt = "true"
    if not inhover then htxt = "false" end
    local text = ""
    if _SETTINGS:IsImperial() then
      local minheight = UTILS.MetersToFeet(self.FixedMinAngels)
      local maxheight = UTILS.MetersToFeet(self.FixedMaxAngels)
      text = string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", minheight, maxheight, htxt)
    else
      local minheight = self.FixedMinAngels
      local maxheight = self.FixedMaxAngels
      text = string.format("Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", minheight, maxheight, htxt)
    end
    self:_SendMessage(text, 10, false, Group)
    return self
  end
    
  --- (Internal) Check if a unit is in a load zone and is hovering in parameters.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:CanHoverLoad(Unit)
    self:T(self.lid .. " CanHoverLoad")
    if self:IsFixedWing(Unit) then return false end
    local outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) and self:IsCorrectHover(Unit)
    if not outcome then
      outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) --and self:IsCorrectHover(Unit)
    end
    return outcome
  end
  
  --- (Internal) Check if a unit is above ground.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:IsUnitInAir(Unit)
    -- get speed and height
    local minheight = self.minimumHoverHeight
    if self.enableFixedWing and self:IsFixedWing(Unit) then
      minheight = 5.1 -- herc is 5m AGL on the ground
    end
    local uheight = Unit:GetHeight()
    local ucoord = Unit:GetCoordinate()
    if not ucoord then
      return false
    end
    local gheight = ucoord:GetLandHeight()
    local aheight = uheight - gheight -- height above ground
    if aheight >= minheight then
      return true
    else
      return false
    end
  end
  
  --- (Internal) Autoload if we can do crates, have capacity free and are in a load zone.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #CTLD self
  function CTLD:AutoHoverLoad(Unit)
    self:T(self.lid .. " AutoHoverLoad")
    -- get capabilities and current load
    local unittype = Unit:GetTypeName()
    local unitname = Unit:GetName()
    local Group = Unit:GetGroup()
    local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
    local cancrates = capabilities.crates -- #boolean
    local cratelimit = capabilities.cratelimit -- #number
    if cancrates then
      -- get load
      local numberonboard = 0
      local loaded = {}
      if self.Loaded_Cargo[unitname] then
        loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
        numberonboard = loaded.Cratesloaded or 0
      end
      local load = cratelimit - numberonboard
      local canload = self:CanHoverLoad(Unit)
      if canload and load > 0 then
        self:_LoadCratesNearby(Group,Unit)
      end
    end
    return self
  end
  
  --- (Internal) Run through all pilots and see if we autoload.
  -- @param #CTLD self
  -- @return #CTLD self
  function CTLD:CheckAutoHoverload()
    if self.hoverautoloading then
      for _,_pilot in pairs (self.CtldUnits) do
        local Unit = UNIT:FindByName(_pilot)
        if self:CanHoverLoad(Unit) then self:AutoHoverLoad(Unit) end
      end
    end
    return self
  end
  
  --- (Internal) Run through DroppedTroops and capture alive units
  -- @param #CTLD self
  -- @return #CTLD self
  function CTLD:CleanDroppedTroops()
    -- Troops
    local troops = self.DroppedTroops
    local newtable = {}
    for _index, _group in pairs (troops) do
      self:T({_group.ClassName})
      if _group and _group.ClassName == "GROUP" then
        if _group:IsAlive() then
          newtable[_index] = _group
        end
      end
    end
    self.DroppedTroops = newtable
    -- Engineers
    local engineers = self.EngineersInField
    local engtable = {}
    for _index, _group in pairs (engineers) do
      self:T({_group.ClassName})
      if _group and _group:IsNotStatus("Stopped") then
        engtable[_index] = _group
      end
    end
    self.EngineersInField = engtable
    return self
  end
  
  --- User - Count both the stock and groups in the field for available cargo types. Counts only limited cargo items and only troops and vehicle/FOB crates!
  -- @param #CTLD self
  -- @param #boolean Restock If true, restock the cargo and troop items.
  -- @param #number Threshold Percentage below which to restock, used in conjunction with Restock (must be true). Defaults to 75 (percent).
  -- @return #table Table A table of contents with numbers.
  -- @usage
  --      The index is the unique cargo name.
  --      Each entry in the returned table contains a table with the following entries:
  --      
  --      {
  --          Stock0 -- number of original stock when the cargo entry was created.
  --          Stock -- number of currently available stock.
  --          StockR -- relative number of available stock, e.g. 75 (percent).
  --          Infield -- number of groups alive in the field of this kind.
  --          Inhelo -- number of troops/crates in any helo alive. Can be with decimals < 1 if e.g. you have cargo that need 4 crates, but you have 2 loaded.
  --          Sum -- sum is stock + infield + inhelo.
  --          GenericCargo -- this filed holds the generic CTLD_CARGO object which drives the available stock. Only populated if Restock is true.
  --        }
  function CTLD:_CountStockPlusInHeloPlusAliveGroups(Restock,Threshold)
    local Troopstable = {}
    for _id, _cargo in pairs(self.Cargo_Crates) do
      local generic = _cargo
      local genname = generic:GetName()
      if generic and generic:GetStock0() > 0 and not Troopstable[genname] then 
        Troopstable[genname] = {
          Stock0 = generic:GetStock0(),
          Stock = generic:GetStock(),
          StockR = generic:GetRelativeStock(),
          Infield = 0,
          Inhelo = 0,
          CratesInfield = 0,
          Sum = generic:GetStock(),
        }
        if Restock == true then
          Troopstable[genname].GenericCargo = generic
        end
      end
    end
    for _id, _cargo in pairs(self.Cargo_Troops) do
      local generic = _cargo
      local genname = generic:GetName()
      if generic and generic:GetStock0() > 0 and not Troopstable[genname] then        
        Troopstable[genname] = {
          Stock0 = generic:GetStock0(),
          Stock = generic:GetStock(),
          StockR = generic:GetRelativeStock(),
          Infield = 0,
          Inhelo = 0,
          CratesInfield = 0,
          Sum = generic:GetStock(),
        }
        if Restock == true then
          Troopstable[genname].GenericCargo = generic
        end
      end
    end   
    for _index, _group in pairs(self.DroppedTroops) do
      if _group and _group:IsAlive() then
        self:T("Looking at " .. _group:GetName() .. " in the field")
        local generic = self:GetGenericCargoObjectFromGroupName(_group:GetName())
        if generic then 
          local genname = generic:GetName()
          self:T("Found Generic " .. genname .. " in the field. Adding.")
          if generic:GetStock0() > 0 then
            Troopstable[genname].Infield = Troopstable[genname].Infield + 1
            Troopstable[genname].Sum = Troopstable[genname].Infield + Troopstable[genname].Stock + Troopstable[genname].Inhelo
          end
        else
          self:E(self.lid .. "Group without Cargo Generic: " .. _group:GetName())
        end
      end
    end
    for _unitname, _loaded in pairs(self.Loaded_Cargo) do
      local _unit = UNIT:FindByName(_unitname)
      if _unit and _unit:IsAlive() then
        local unitname = _unit:GetName()
        local loadedcargo = self.Loaded_Cargo[unitname].Cargo or {}
        for _, _cgo in pairs(loadedcargo) do
          local cargo = _cgo
          local type = cargo.CargoType
          local gname = cargo.Name
          local gcargo = self:_FindCratesCargoObject(gname) or self:_FindTroopsCargoObject(gname)
          self:T("Looking at " .. gname .. " in the helo - type = " .. type)
          if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS or type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then
            if gcargo and gcargo:GetStock0() > 0 then
              self:T("Adding " .. gname .. " in the helo - type = " .. type)
              if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then
                Troopstable[gname].Inhelo = Troopstable[gname].Inhelo + 1
              end
              if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then
                local counting = gcargo.CratesNeeded
                local added = 1
                if counting > 1 then
                  added = added / counting
                end
                Troopstable[gname].Inhelo = Troopstable[gname].Inhelo + added
              end
              Troopstable[gname].Sum = Troopstable[gname].Infield + Troopstable[gname].Stock + Troopstable[gname].Inhelo + Troopstable[gname].CratesInfield
            end
          end
        end
      end
    end 
    if self.Spawned_Cargo then
      -- First pass: just add fractional amounts for each crate on the ground
      for i = #self.Spawned_Cargo, 1, -1 do
        local cargo = self.Spawned_Cargo[i]
        if cargo and cargo:GetPositionable() and cargo:GetPositionable():IsAlive() then
          local genname = cargo:GetName()
          local gcargo  = self:_FindCratesCargoObject(genname)
          if Troopstable[genname] and gcargo and gcargo:GetStock0() > 0 then
            local needed = gcargo.CratesNeeded or 1
            local added  = 1
            if needed > 1 then
              added = added / needed
            end
            Troopstable[genname].CratesInfield = Troopstable[genname].CratesInfield + added
            Troopstable[genname].Sum = Troopstable[genname].Infield + Troopstable[genname].Stock
                                     + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield
          end
        end
      end
      for i = #self.Spawned_Cargo, 1, -1 do
        local cargo = self.Spawned_Cargo[i]
        if cargo and cargo:GetPositionable() and cargo:GetPositionable():IsAlive() then
          local genname = cargo:GetName()
          if Troopstable[genname] then
            if Troopstable[genname].Inhelo == 0 and Troopstable[genname].CratesInfield < 1 then
              Troopstable[genname].CratesInfield = 0
              Troopstable[genname].Sum = Troopstable[genname].Stock
              cargo:GetPositionable():Destroy(false)
              table.remove(self.Spawned_Cargo, i)
              local leftover = Troopstable[genname].Stock0 - (Troopstable[genname].Infield + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield)
              if leftover < Troopstable[genname].Stock then
                Troopstable[genname].Stock = leftover
              end
              Troopstable[genname].Sum = Troopstable[genname].Stock + Troopstable[genname].Infield + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield
            end
          end
        end
      end
    end
      if Restock == true then
        local threshold = Threshold or 75
        for _name,_data in pairs(Troopstable) do
          if _data.StockR and _data.StockR < threshold then
            if _data.GenericCargo then
              _data.GenericCargo:SetStock(_data.Stock0) -- refill to start level
            end
          end
        end
      end
    return Troopstable
  end
  

  --- User - function to add stock of a certain troops type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:AddStockTroops(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:AddStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to add stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:AddStockCrates(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:AddStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to add stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:AddStockStatics(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:AddStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to set the stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to be available. Nil equals unlimited
  -- @return #CTLD self
  function CTLD:SetStockCrates(Name, Number)
    local name = Name or "none"
    local number = Number
    -- find right generic type
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:SetStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to set the stock of a certain troops type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to be available. Nil equals unlimited
  -- @return #CTLD self
  function CTLD:SetStockTroops(Name, Number)
    local name = Name or "none"
    local number = Number
    -- find right generic type
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:SetStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to set the stock of a certain statics type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to be available. Nil equals unlimited
  -- @return #CTLD self
  function CTLD:SetStockStatics(Name, Number)
    local name = Name or "none"
    local number = Number
    -- find right generic type
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:SetStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to get a table of crates in stock
  -- @param #CTLD self
  -- @return #table Table Table of Stock, indexed by cargo type name
  function CTLD:GetStockCrates()
    local Stock = {}
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      Stock[_troop.Name] = _troop.Stock or -1
      --table.insert(Stock,_troop.Name,_troop.Stock or -1)
    end
    return Stock
  end  
  
  --- User - function to get a table of troops in stock
  -- @param #CTLD self
  -- @return #table Table Table of Stock, indexed by cargo type name
  function CTLD:GetStockTroops()
    local Stock = {}
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      Stock[_troop.Name] = _troop.Stock or -1
      --table.insert(Stock,_troop.Name,_troop.Stock or -1)
    end
    return Stock
  end
  
  --- User - Query the cargo loaded from a specific unit
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit The (client) unit to query.
  -- @return #number Troopsloaded
  -- @return #number Cratesloaded
  -- @return #table Cargo Table of #CTLD_CARGO objects
  function CTLD:GetLoadedCargo(Unit)
    local Troops = 0
    local Crates = 0
    local Cargo = {}
    if Unit and Unit:IsAlive() then
      local name = Unit:GetName()
      if self.Loaded_Cargo[name] then
        Troops = self.Loaded_Cargo[name].Troopsloaded or 0
        Crates = self.Loaded_Cargo[name].Cratesloaded or 0
        Cargo = self.Loaded_Cargo[name].Cargo or {}
      end
    end
    return Troops, Crates, Cargo
  end
  
  --- User - function to get a table of statics cargo in stock
  -- @param #CTLD self
  -- @return #table Table Table of Stock, indexed by cargo type name
  function CTLD:GetStockStatics()
    local Stock = {}
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      Stock[_troop.Name] = _troop.Stock or -1
      -- table.insert(Stock,_troop.Name,_troop.Stock or -1)
    end
    return Stock
  end
  
  --- User - function to remove stock of a certain troops type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:RemoveStockTroops(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:RemoveStock(number)
      end
    end
    return self
  end
  
  --- User - function to remove stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:RemoveStockCrates(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:RemoveStock(number)
      end
    end
    return self
  end
  
  --- User - function to remove stock of a certain statics type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:RemoveStockStatics(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:RemoveStock(number)
      end
    end
    return self
  end
  
  --- (User) Get a generic #CTLD_CARGO entry from a group name, works for Troops and Vehicles, FOB, i.e. everything that is spawned as a GROUP object.
  -- @param #CTLD self
  -- @param #string GroupName The name to use for the search
  -- @return #CTLD_CARGO The cargo object or nil if not found
  function CTLD:GetGenericCargoObjectFromGroupName(GroupName)
    local Cargotype = nil
    local template = GroupName
    if string.find(template,"#") then
      template = string.gsub(GroupName,"#(%d+)$","")
    end   
    template = string.gsub(template,"-(%d+)$","")
    for k,v in pairs(self.Cargo_Troops) do
    local comparison = ""
    if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
      if comparison == template then
        Cargotype = v
        break
      end
    end
    if not Cargotype then
      for k,v in pairs(self.Cargo_Crates) do -- #number, #CTLD_CARGO
      local comparison = ""
      if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
        if comparison == template and v.CargoType ~= CTLD_CARGO.Enum.REPAIR then
          Cargotype = v
          break
        end
      end
    end
    return Cargotype
  end
  
  --- (Internal) Check on engineering teams
  -- @param #CTLD self
  -- @return #CTLD self
  function CTLD:_CheckEngineers()
    self:T(self.lid.." CheckEngineers")
    local engtable = self.EngineersInField
    for _ind,_engineers in pairs (engtable) do
      local engineers = _engineers -- #CTLD_ENGINEERING
      local wrenches = engineers.Group -- Wrapper.Group#GROUP
      self:T(_engineers.lid .. _engineers:GetStatus())
      if wrenches and wrenches:IsAlive() then
        if engineers:IsStatus("Running") or engineers:IsStatus("Searching") then
          local crates,number = self:_FindCratesNearby(wrenches,nil, self.EngineerSearch,true,true) -- #table
          engineers:Search(crates,number)
        elseif engineers:IsStatus("Moving") then
          engineers:Move()
        elseif engineers:IsStatus("Arrived") then
          engineers:Build()
          local unit = wrenches:GetUnit(1)
          self:_BuildCrates(wrenches,unit,true)
          self:_RepairCrates(wrenches,unit,true)
          engineers:Done()
        end
      else
        engineers:Stop()
      end
    end
    return self
  end
  
  --- (User) Pre-populate troops in the field.
  -- @param #CTLD self
  -- @param Core.Zone#ZONE Zone The zone where to drop the troops.
  -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn.
  -- @param #table Surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type!
  -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another!
  -- @param #string Structure (Optional) String object describing the current structure of the injected group; mainly for load/save to keep current state setup.
  -- @param #number TimeStamp (Optional) Timestamp used internally on re-loading from disk.
  -- @return #CTLD self
  -- @usage Use this function to pre-populate the field with Troops or Engineers at a random coordinate in a zone:
  --            -- create a matching #CTLD_CARGO type
  --            local InjectTroopsType = CTLD_CARGO:New(nil,"Infantry",{"Inf12"},CTLD_CARGO.Enum.TROOPS,true,true,12,nil,false,80)
  --            -- get a #ZONE object
  --            local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE
  --            -- and go:
  --            my_ctld:InjectTroops(dropzone,InjectTroopsType,{land.SurfaceType.LAND})
  function CTLD:InjectTroops(Zone,Cargo,Surfacetypes,PreciseLocation,Structure,TimeStamp)
    self:T(self.lid.." InjectTroops")
    local cargo = Cargo -- #CTLD_CARGO
    
    local function IsTroopsMatch(cargo)
      local match = false
      local cgotbl = self.Cargo_Troops
      local name = cargo:GetName()
      local CargoObject
      local CargoName
      for _,_cgo in pairs (cgotbl) do
        local cname = _cgo:GetName()
        if name == cname then
          match = true
          CargoObject = _cgo
          CargoName = cname
          break
        end
      end
      return match, CargoObject, CargoName
    end
    
    local function Cruncher(group,typename,anzahl)
      local units = group:GetUnits()
      local reduced = 0
      for _,_unit in pairs (units) do
        local typo = _unit:GetTypeName()
        if typename == typo then
          _unit:Destroy(false)
          reduced = reduced + 1
          if reduced == anzahl then break end
        end
      end
    end
    
    local function PostSpawn(args)
      local group = args[1]
      local structure = args[2]
      if structure then
  
        local loadedstructure = {}
        local strcset = UTILS.Split(structure,";")
        for _,_data in pairs(strcset) do
          local datasplit = UTILS.Split(_data,"==")
          loadedstructure[datasplit[1]] = tonumber(datasplit[2])
        end
  
        local originalstructure = UTILS.GetCountPerTypeName(group)
  
        for _name,_number in pairs(originalstructure) do
          local loadednumber = 0
          if loadedstructure[_name] then
            loadednumber = loadedstructure[_name]
          end
          local reduce = false
          if loadednumber < _number then reduce = true end
          
          if reduce then
            Cruncher(group,_name,_number-loadednumber)  
          end
                    
        end
     end
    end
    
    local match,CargoObject,CargoName = IsTroopsMatch(cargo)
    
    if not match then
      self.CargoCounter = self.CargoCounter + 1
      cargo.ID = self.CargoCounter
      --cargo.Stock = 1
      table.insert(self.Cargo_Troops,cargo)
    end
    
    if match and CargoObject then
      local stock = CargoObject:GetStock()
      if stock ~= -1 and stock ~= nil and stock == 0 then
       -- stock empty
       self:T(self.lid.."Stock of "..CargoName.." is empty. Cannot inject.")
       return
      else
        CargoObject:RemoveStock(1)
      end
    end
    
    local type = cargo:GetType() -- #CTLD_CARGO.Enum
    if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then
      -- unload 
      local name = cargo:GetName() or "none"
      local temptable = cargo:GetTemplates() or {}
      local factor = 1.5
      local zone = Zone
      local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2()
      if PreciseLocation then
        randomcoord = zone:GetCoordinate():GetVec2()
      end
      local randompositions = not PreciseLocation
      for _,_template in pairs(temptable) do
        self.TroopCounter = self.TroopCounter + 1
        local alias = string.format("%s-%d", _template, math.random(1,100000))
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
          :InitRandomizeUnits(randompositions,20,2)
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :InitDelayOff()
          :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
          :SpawnFromVec2(randomcoord)
        if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then
          self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter])
        end
      end -- template loop
      cargo:SetWasDropped(true)
      -- engineering group?
      if type == CTLD_CARGO.Enum.ENGINEERS then
        self.Engineers = self.Engineers + 1
        local grpname = self.DroppedTroops[self.TroopCounter]:GetName()
        self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname)
      end
      
      if Structure then
        BASE:ScheduleOnce(0.5,PostSpawn,{self.DroppedTroops[self.TroopCounter],Structure})
      end
      
      if self.keeploadtable and TimeStamp ~= nil then
        self:T2("Inserting: "..cargo.CargoType)
        local cargotype = type
        table.insert(self.LoadedGroupsTable,{Group=self.DroppedTroops[self.TroopCounter], TimeStamp=TimeStamp, CargoType=cargotype, CargoName=name})
      end
      
      if self.eventoninject then
         self:__TroopsDeployed(1,nil,nil,self.DroppedTroops[self.TroopCounter],type)
      end
    end -- if type end
    return self
  end
  
  --- (User) Pre-populate vehicles in the field.
  -- @param #CTLD self
  -- @param Core.Zone#ZONE Zone The zone where to drop the troops.
  -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn.
  -- @param #table Surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type!
  -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another!
  -- @param #string Structure (Optional) String object describing the current structure of the injected group; mainly for load/save to keep current state setup.
  -- @param #number TimeStamp (Optional) Timestamp used internally on re-loading from disk.
  -- @return #CTLD self
  -- @usage Use this function to pre-populate the field with Vehicles or FOB at a random coordinate in a zone:
  --            -- create a matching #CTLD_CARGO type
  --            local InjectVehicleType = CTLD_CARGO:New(nil,"Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000)
  --            -- get a #ZONE object
  --            local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE
  --            -- and go:
  --            my_ctld:InjectVehicles(dropzone,InjectVehicleType)
  function CTLD:InjectVehicles(Zone,Cargo,Surfacetypes,PreciseLocation,Structure,TimeStamp)
    self:T(self.lid.." InjectVehicles")
    local cargo = Cargo -- #CTLD_CARGO
    
    local function IsVehicMatch(cargo)
      local match = false
      local cgotbl = self.Cargo_Crates
      local name = cargo:GetName()
      local CargoObject
      local CargoName
      for _,_cgo in pairs (cgotbl) do
        local cname = _cgo:GetName()
        if name == cname then
          match = true
          CargoObject = _cgo
          CargoName = cname
          break
        end
      end
      return match,CargoObject,CargoName
    end
    
    local function Cruncher(group,typename,anzahl)
      local units = group:GetUnits()
      local reduced = 0
      for _,_unit in pairs (units) do
        local typo = _unit:GetTypeName()
        if typename == typo then
          _unit:Destroy(false)
          reduced = reduced + 1
          if reduced == anzahl then break end
        end
      end
    end
    
    local function PostSpawn(args)
      local group = args[1]
      local structure = args[2]
      if structure then
  
        local loadedstructure = {}
        local strcset = UTILS.Split(structure,";")
        for _,_data in pairs(strcset) do
          local datasplit = UTILS.Split(_data,"==")
          loadedstructure[datasplit[1]] = tonumber(datasplit[2])
        end
  
        local originalstructure = UTILS.GetCountPerTypeName(group)
  
        for _name,_number in pairs(originalstructure) do
          local loadednumber = 0
          if loadedstructure[_name] then
            loadednumber = loadedstructure[_name]
          end
          local reduce = false
          if loadednumber < _number then reduce = true end
          
          if reduce then
            Cruncher(group,_name,_number-loadednumber)  
          end
                    
        end
     end
    end
    
    local match,CargoObject,CargoName = IsVehicMatch(cargo)
    
    if not match then
      self.CargoCounter = self.CargoCounter + 1
      cargo.ID = self.CargoCounter
      --cargo.Stock = 1
      table.insert(self.Cargo_Crates,cargo)
    end
    
    if match and CargoObject then
      local stock = CargoObject:GetStock()
      if stock ~= -1 and stock ~= nil and stock == 0 then
       -- stock empty
       self:T(self.lid.."Stock of "..CargoName.." is empty. Cannot inject.")
       return
      else
        CargoObject:RemoveStock(1)
      end
    end
    
    local type = cargo:GetType() -- #CTLD_CARGO.Enum
    if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then
      -- unload 
      local name = cargo:GetName() or "none"
      local temptable = cargo:GetTemplates() or {}
      local factor = 1.5
      local zone = Zone
      local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2()
      if PreciseLocation then
        randomcoord = zone:GetCoordinate():GetVec2()
      end
      cargo:SetWasDropped(true)
      local canmove = false
      if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end
      for _,_template in pairs(temptable) do
        self.TroopCounter = self.TroopCounter + 1
        local alias = string.format("%s-%d", _template, math.random(1,100000))
        if canmove then
          self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
            :InitRandomizeUnits(true,20,2)
            :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
            :InitDelayOff()
            :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
            :SpawnFromVec2(randomcoord)
        else -- don't random position of e.g. SAM units build as FOB
          self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
            :InitDelayOff()
            :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
            :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
            :SpawnFromVec2(randomcoord)
        end
        
        if Structure then
          BASE:ScheduleOnce(0.5,PostSpawn,{self.DroppedTroops[self.TroopCounter],Structure})
        end
        
        if self.keeploadtable and TimeStamp ~= nil then
          self:T2("Inserting: "..cargo.CargoType)
          local cargotype = type
          table.insert(self.LoadedGroupsTable,{Group=self.DroppedTroops[self.TroopCounter], TimeStamp=TimeStamp, CargoType=cargotype, CargoName=name})
        end
        
        if self.eventoninject then
          self:__CratesBuild(1,nil,nil,self.DroppedTroops[self.TroopCounter])
        end
      end -- end loop
    end -- if type end
    return self
  end
  
------------------------------------------------------------------- 
-- TODO FSM functions
------------------------------------------------------------------- 

  --- (Internal) FSM Function onafterStart.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onafterStart(From, Event, To)
    self:T({From, Event, To})
    self:I(self.lid .. "Started ("..self.version..")")
    if self.enableHercules then self.enableFixedWing = true end
    if self.UserSetGroup then
      self.PilotGroups  = self.UserSetGroup
    elseif self.useprefix or self.enableFixedWing then
      local prefix = self.prefixes
      if self.enableFixedWing then
        self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterStart()
      else
        self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterCategories("helicopter"):FilterStart()
      end
    else
      self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategories("helicopter"):FilterStart()
    end
    -- Events
    self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
    self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler)
    self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler)
    self:HandleEvent(EVENTS.UnitLost, self._EventHandler)  
    --self:HandleEvent(EVENTS.Birth, self._EventHandler)
    self:HandleEvent(EVENTS.NewDynamicCargo, self._EventHandler)
    self:HandleEvent(EVENTS.DynamicCargoLoaded, self._EventHandler)  
    self:HandleEvent(EVENTS.DynamicCargoUnloaded, self._EventHandler)  
    self:HandleEvent(EVENTS.DynamicCargoRemoved, self._EventHandler)     
    self:HandleEvent(EVENTS.Land, self._EventHandler)
    self:HandleEvent(EVENTS.Takeoff, self._EventHandler)
    self:__Status(-5)
    
    -- AutoSave
    if self.enableLoadSave then
      local interval = self.saveinterval
      local filename = self.filename
      local filepath = self.filepath
      self:__Save(interval,filepath,filename)
    end
    
    if type(self.VehicleMoveFormation) == "table" then
      local Formations = {}
      for _,_formation in pairs(self.VehicleMoveFormation) do
        table.insert(Formations,_formation)
      end
      self.VehicleMoveFormation = nil
      self.VehicleMoveFormation = Formations
    end
  
    return self
  end

  --- (Internal) FSM Function onbeforeStatus.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onbeforeStatus(From, Event, To)
    self:T({From, Event, To})
    self:CleanDroppedTroops()
    self:_RefreshF10Menus()
    self:CheckDroppedBeacons()
    self:_RefreshRadioBeacons()
    self:CheckAutoHoverload()
    self:_CheckEngineers()
    return self
  end
  
  --- (Internal) FSM Function onafterStatus.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onafterStatus(From, Event, To)
    self:T({From, Event, To})
     -- gather some stats
    -- pilots
    local pilots = 0
    for _,_pilot in pairs (self.CtldUnits) do   
     pilots = pilots + 1
    end
     
    -- spawned cargo boxes curr in field
    local boxes = 0
    for _,_pilot in pairs (self.Spawned_Cargo) do
     boxes = boxes + 1
    end
    
    local cc =  self.CargoCounter
    local tc = self.TroopCounter
    
    if self.debug or self.verbose > 0 then 
      local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc)
      local m = MESSAGE:New(text,10,"CTLD"):ToAll()
      if self.verbose > 0 then
        self:I(self.lid.."Cargo and Troops in Stock:")
        for _,_troop in pairs (self.Cargo_Crates) do
          local name = _troop:GetName()
          local stock = _troop:GetStock()
          self:I(string.format("-- %s \t\t\t %d", name, stock))
        end
        for _,_troop in pairs (self.Cargo_Statics) do
          local name = _troop:GetName()
          local stock = _troop:GetStock()
          self:I(string.format("-- %s \t\t\t %d", name, stock))
        end
        for _,_troop in pairs (self.Cargo_Troops) do
          local name = _troop:GetName()
          local stock = _troop:GetStock()
          self:I(string.format("-- %s \t\t %d", name, stock))
        end
      end
    end
    self:__Status(-30)
    return self
  end
  
  --- (Internal) FSM Function onafterStop.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onafterStop(From, Event, To)
    self:T({From, Event, To})
    self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
    self:UnHandleEvent(EVENTS.PlayerEnterUnit)
    self:UnHandleEvent(EVENTS.PlayerLeaveUnit)
    self:UnHandleEvent(EVENTS.UnitLost)  
    self:UnHandleEvent(EVENTS.Shot) 
    return self
  end
  
  --- (Internal) FSM Function onbeforeTroopsPickedUp.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsPickedUp(From, Event, To, Group, Unit, Cargo)
    self:T({From, Event, To})
    return self
  end
  
    --- (Internal) FSM Function onbeforeCratesPickedUp.
  -- @param #CTLD self
  -- @param #string From State .
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded!
  -- @return #CTLD self
  function CTLD:onbeforeCratesPickedUp(From, Event, To, Group, Unit, Cargo)
    self:T({From, Event, To})
    return self
  end
  
  --- (Internal) FSM Function onbeforeTroopsExtracted.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @param #string Groupname Name of the extracted #GROUP.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsExtracted(From, Event, To, Group, Unit, Troops, Groupname)
    self:T({From, Event, To})
    if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
      local playername = Unit:GetPlayerName()
      --local dropcoord = Troops:GetCoordinate() or COORDINATE:New(0,0,0)
      --local dropvec2 = dropcoord:GetVec2()
      self.PlayerTaskQueue:ForEach(
        function (Task)
          local task = Task -- Ops.PlayerTask#PLAYERTASK
          local subtype = task:GetSubType()
          -- right subtype?
          if Event == subtype and not task:IsDone() then
            local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case ....
            self:T2({Name=Groupname,Property=task:GetProperty("ExtractName")})
            if task:GetProperty("ExtractName") then
              local okaygroup = string.find(Groupname,task:GetProperty("ExtractName"),1,true)
              if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and okaygroup then
                if task.Clients:HasUniqueID(playername) then
                  -- success
                  task:__Success(-1)
                end
              end
            else
              self:T({Text="'ExtractName' Property not set",Name=Groupname,Property=task.Type})
            end
          end
        end
      )
    end
    return self
  end
    
    
  --- (Internal) FSM Function onbeforeTroopsDeployed.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsDeployed(From, Event, To, Group, Unit, Troops)
    self:T({From, Event, To})
    if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
      local playername = Unit:GetPlayerName()
      local dropcoord = Troops:GetCoordinate() or COORDINATE:New(0,0,0)
      local dropvec2 = dropcoord:GetVec2()
      self.PlayerTaskQueue:ForEach(
        function (Task)
          local task = Task -- Ops.PlayerTask#PLAYERTASK
          local subtype = task:GetSubType()
          -- right subtype?
          if Event == subtype and not task:IsDone() then
            local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case ....
            if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2) then
              if task.Clients:HasUniqueID(playername) then
                -- success
                task:__Success(-1)
              end
            end
          end
        end
      )
    end
    return self
  end
  
  --- (Internal) FSM Function onafterTroopsDeployed.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @param #CTLD.CargoZoneType Type Type of Cargo deployed
  -- @return #CTLD self
  function CTLD:onafterTroopsDeployed(From, Event, To, Group, Unit, Troops, Type)
    self:T({From, Event, To})
    if self.movetroopstowpzone and Type ~= CTLD_CARGO.Enum.ENGINEERS then
      self:_MoveGroupToZone(Troops)
    end
    return self
  end
  
  --- (Internal) FSM Function onbeforeCratesDropped.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded!
  -- @return #CTLD self
  function CTLD:onbeforeCratesDropped(From, Event, To, Group, Unit, Cargotable)
    self:T({From, Event, To})
    return self
  end
  
  --- (Internal) FSM Function onbeforeCratesBuild.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self
  function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle)
    self:T({From, Event, To})
    if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
      local playername = Unit:GetPlayerName()
      local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0)
      local dropvec2 = dropcoord:GetVec2()
      self.PlayerTaskQueue:ForEach(
        function (Task)
          local task = Task -- Ops.PlayerTask#PLAYERTASK
          local subtype = task:GetSubType()
          -- right subtype?
          if Event == subtype and not task:IsDone() then
            local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case ....
            if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2) then
              if task.Clients:HasUniqueID(playername) then
                -- success
                task:__Success(-1)
              end
            end
          end
        end
      )
    end
    return self
  end

  --- (Internal) FSM Function onafterCratesBuild.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self
  function CTLD:onafterCratesBuild(From, Event, To, Group, Unit, Vehicle)
    self:T({From, Event, To})
    if self.movetroopstowpzone then
      self:_MoveGroupToZone(Vehicle)
    end
    return self
  end
  
  --- (Internal) FSM Function onbeforeTroopsRTB.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #string ZoneName Name of the Zone where the Troops have been RTB'd.
  -- @param Core.Zone#ZONE_Radius ZoneObject of the Zone where the Troops have been RTB'd.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsRTB(From, Event, To, Group, Unit, ZoneName, ZoneObject)
    self:T({From, Event, To})
    return self
  end
  
  --- On before "Save" event. Checks if io and lfs are available.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for saving. Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onbeforeSave(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    if not self.enableLoadSave then
      return self
    end
    -- Thanks to @FunkyFranky 
    -- Check io module is available.
    if not io then
      self:E(self.lid.."ERROR: io not desanitized. Can't save current state.")
      return false
    end
  
    -- Check default path.
    if path==nil and not lfs then
      self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
    end
  
    return true
  end
  
  --- On after "Save" event. Player data is saved to file.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized.
  -- @param #string filename (Optional) File name for saving. Default is Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onafterSave(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    -- Thanks to @FunkyFranky 
    if not self.enableLoadSave then
      return self
    end
    --- Function that saves data to file
    local function _savefile(filename, data)
      local f = assert(io.open(filename, "wb"))
      f:write(data)
      f:close()
    end
  
    -- Set path or default.
    if lfs then
      path=self.filepath or lfs.writedir()
    end
    
    -- Set file name.
    filename=filename or self.filename
  
    -- Set path.
    if path~=nil then
      filename=path.."\\"..filename
    end
    
    local grouptable = self.DroppedTroops -- #table
    local cgovehic = self.Cargo_Crates
    local cgotable = self.Cargo_Troops
    local stcstable = self.Spawned_Cargo
    
    local statics = nil
    local statics = {}
    self:T(self.lid.."Building Statics Table for Saving")
    for _,_cargo in pairs (stcstable) do     
      local cargo = _cargo -- #CTLD_CARGO
      local object = cargo:GetPositionable() -- Wrapper.Static#STATIC
      if object and object:IsAlive() and (cargo:WasDropped() or not cargo:HasMoved()) then
        statics[#statics+1] = cargo
      end
    end
    
    -- find matching cargo
    local function FindCargoType(name,table)
      -- name matching a template in the table
      local match = false
      local cargo = nil
      name = string.gsub(name,"-"," ")
      for _ind,_cargo in pairs (table) do
        local thiscargo = _cargo -- #CTLD_CARGO
        local template = thiscargo:GetTemplates()
        if type(template) == "string" then
          template = { template }
        end
        for _,_name in pairs (template) do
          _name = string.gsub(_name,"-"," ")
          if string.find(name,_name) and _cargo:GetType() ~= CTLD_CARGO.Enum.REPAIR then
            match = true
            cargo = thiscargo
          end
        end
        if match then break end
      end
      return match, cargo
    end
    
      
    --local data = "LoadedData = {\n"
    local data = "Group,x,y,z,CargoName,CargoTemplates,CargoType,CratesNeeded,CrateMass,Structure,StaticCategory,StaticType,StaticShape,SpawnTime\n"
    local n = 0
    for _,_grp in pairs(grouptable) do
      local group = _grp -- Wrapper.Group#GROUP
      if group and group:IsAlive() then
        -- get template name
        local name = group:GetName()
        local template = name
        
        if string.find(template,"#") then
          template = string.gsub(name,"#(%d+)$","")
        end
        
        local template = string.gsub(name,"-(%d+)$","")
        
        local match, cargo = FindCargoType(template,cgotable)
        if not match then
          match, cargo = FindCargoType(template,cgovehic)
        end
        if match then
          n = n + 1
          local cargo = cargo -- #CTLD_CARGO
          local cgoname = cargo.Name
          local cgotemp = cargo.Templates
          local cgotype = cargo.CargoType
          local cgoneed = cargo.CratesNeeded
          local cgomass = cargo.PerCrateMass
          local scat,stype,sshape = cargo:GetStaticTypeAndShape()
          local structure = UTILS.GetCountPerTypeName(group)
          local strucdata =  ""
          for typen,anzahl in pairs (structure) do
            strucdata = strucdata .. typen .. "=="..anzahl..";"
          end
          local spawntime = group.spawntime or timer.getTime()+n
          
          if type(cgotemp) == "table" then       
            local templates = "{"
            for _,_tmpl in pairs(cgotemp) do
              templates = templates .. _tmpl .. ";"
            end
            templates = templates .. "}"
            cgotemp = templates
          end
          
          local location = group:GetVec3()
          local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,%s,%s,%s,%s,%f\n"
              ,template,location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,strucdata,scat,stype,sshape or "none",spawntime)             
          data = data .. txt
        end
      end
    end
    
    for _,_cgo in pairs(statics) do
      local object = _cgo -- #CTLD_CARGO
      local cgoname = object.Name
      local cgotemp = object.Templates
      
      if type(cgotemp) == "table" then       
        local templates = "{"
        for _,_tmpl in pairs(cgotemp) do
          templates = templates .. _tmpl .. ";"
        end
        templates = templates .. "}"
        cgotemp = templates
      end
      
      local cgotype = object.CargoType
      local cgoneed = object.CratesNeeded
      local cgomass = object.PerCrateMass
      local crateobj = object.Positionable
      local location = crateobj:GetVec3()
      local scat,stype,sshape = object:GetStaticTypeAndShape()
      local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,'none',%s,%s,%s\n"
          ,"STATIC",location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,scat,stype,sshape or "none")
      data = data .. txt
    end
    
    _savefile(filename, data)
     
    -- AutoSave
    if self.enableLoadSave then
      local interval = self.saveinterval
      local filename = self.filename
      local filepath = self.filepath
      self:__Save(interval,filepath,filename)
    end
    return self
  end

  --- On before "Load" event. Checks if io and lfs and the file are available.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for loading. Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onbeforeLoad(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    if not self.enableLoadSave then
      return self
    end
    --- Function that check if a file exists.
    local function _fileexists(name)
       local f=io.open(name,"r")
       if f~=nil then
        io.close(f)
        return true
      else
        return false
      end
    end
    
    -- Set file name and path
    filename=filename or self.filename
    path = path or self.filepath
    
    -- Check io module is available.
    if not io then
      self:E(self.lid.."WARNING: io not desanitized. Cannot load file.")
      return false
    end
  
    -- Check default path.
    if path==nil and not lfs then
      self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
    end
  
    -- Set path or default.
    if lfs then
      path=path or lfs.writedir()
    end
  
    -- Set path.
    if path~=nil then
      filename=path.."\\"..filename
    end
  
    -- Check if file exists.
    local exists=_fileexists(filename)
  
    if exists then
      return true
    else
      self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename))
      return false
      --return self
    end
  
  end

  --- On after "Load" event. Loads dropped units from file.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for loading. Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onafterLoad(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    if not self.enableLoadSave then
      return self
    end
    --- Function that loads data from a file.
    local function _loadfile(filename)
      local f=assert(io.open(filename, "rb"))
      local data=f:read("*all")
      f:close()
      return data
    end
    
    -- Set file name and path
    filename=filename or self.filename
    path = path or self.filepath
    
    -- Set path or default.
    if lfs then
      path=path or lfs.writedir()
    end
  
    -- Set path.
    if path~=nil then
      filename=path.."\\"..filename
    end
  
    -- Info message.
    local text=string.format("Loading CTLD state from file %s", filename)
    MESSAGE:New(text,10):ToAllIf(self.Debug)
    self:I(self.lid..text)
    
    local file=assert(io.open(filename, "rb"))
    
    local loadeddata = {}
    for line in file:lines() do
        loadeddata[#loadeddata+1] = line
    end
    file:close()
    
    -- remove header
    table.remove(loadeddata, 1)
    local n=0
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")     
      -- 1=Group,2=x,3=y,4=z,5=CargoName,6=CargoTemplates,7=CargoType,8=CratesNeeded,9=CrateMass,10=Structure,11=StaticCategory,12=StaticType,13=StaticShape,14=Timestamp
      local groupname = dataset[1]
      local vec2 = {}
      vec2.x = tonumber(dataset[2])
      vec2.y = tonumber(dataset[4])
      local cargoname = dataset[5]
      local cargotemplates = dataset[6]
      local cargotype = dataset[7]
      local size = tonumber(dataset[8])
      local mass = tonumber(dataset[9])
      local StaticCategory = dataset[11]
      local StaticType = dataset[12]
      local StaticShape = dataset[13]
      n=n+1
      local timestamp = tonumber(dataset[14]) or (timer.getTime()+n)
      self:T2("TimeStamp = "..timestamp)
      if type(groupname) == "string" and groupname ~= "STATIC" then
        cargotemplates = string.gsub(cargotemplates,"{","")
        cargotemplates = string.gsub(cargotemplates,"}","")
        cargotemplates = UTILS.Split(cargotemplates,";")
        local structure = nil
        if dataset[10] and dataset[10] ~= "none" then
          structure = dataset[10]
          structure = string.gsub(structure,",","")
        end
        -- inject at Vec2
        local dropzone = ZONE_RADIUS:New("DropZone",vec2,20)
        if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then
          local injectvehicle = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass)
          injectvehicle:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape)      
          self:InjectVehicles(dropzone,injectvehicle,self.surfacetypes,self.useprecisecoordloads,structure,timestamp)
        elseif cargotype == CTLD_CARGO.Enum.TROOPS or cargotype == CTLD_CARGO.Enum.ENGINEERS then
          local injecttroops = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass)      
          self:InjectTroops(dropzone,injecttroops,self.surfacetypes,self.useprecisecoordloads,structure,timestamp)
        end
      elseif (type(groupname) == "string" and groupname == "STATIC") or cargotype == CTLD_CARGO.Enum.REPAIR then
        local dropzone = ZONE_RADIUS:New("DropZone",vec2,20)
        local injectstatic = nil
        if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then
          cargotemplates = string.gsub(cargotemplates,"{","")
          cargotemplates = string.gsub(cargotemplates,"}","")
          cargotemplates = UTILS.Split(cargotemplates,";")
          injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) 
          injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape)     
        elseif cargotype == CTLD_CARGO.Enum.STATIC or cargotype == CTLD_CARGO.Enum.REPAIR then
          injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass)
          injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape)
          local map=cargotype:GetStaticResourceMap()
          injectstatic:SetStaticResourceMap(map) 
        end
        if injectstatic then
          self:InjectStatics(dropzone,injectstatic,false,true)
        end
      end    
    end
    if self.keeploadtable then -- keeploadtables
      self:__Loaded(1,self.LoadedGroupsTable)
    end
    return self
  end
end -- end do

do 
--- **Hercules Cargo AIR Drop Events** by Anubis Yinepu
-- Moose CTLD OO refactoring by Applevangelist
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO CTLD_HERCULES
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- This script will only work for the Herculus mod by Anubis, and only for **Air Dropping** cargo from the Hercules. 
-- Use the standard Moose CTLD if you want to unload on the ground.
-- Payloads carried by pylons 11, 12 and 13 need to be declared in the Herculus_Loadout.lua file
-- Except for Ammo pallets, this script will spawn whatever payload gets launched from pylons 11, 12 and 13
-- Pylons 11, 12 and 13 are moveable within the Herculus cargobay area
-- Ammo pallets can only be jettisoned from these pylons with no benefit to DCS world
-- To benefit DCS world, Ammo pallets need to be off/on loaded using DCS arming and refueling window
-- Cargo_Container_Enclosed = true: Cargo enclosed in container with parachute, need to be dropped from 100m (300ft) or more, except when parked on ground
-- Cargo_Container_Enclosed = false: Open cargo with no parachute, need to be dropped from 10m (30ft) or less

------------------------------------------------------
--- **CTLD_HERCULES** class, extends Core.Base#BASE
-- @type CTLD_HERCULES
-- @field #string ClassName
-- @field #string lid
-- @field #string Name
-- @field #string Version
-- @extends Core.Base#BASE
CTLD_HERCULES = {
  ClassName = "CTLD_HERCULES",
  lid = "",
  Name = "",
  Version = "0.0.3",
}

--- Define cargo types.
-- @type CTLD_HERCULES.Types
-- @field #table Type Name of cargo type, container (boolean) in container or not.
CTLD_HERCULES.Types = {
  ["ATGM M1045 HMMWV TOW Air [7183lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = true},
  ["ATGM M1045 HMMWV TOW Skid [7073lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = false},
  ["APC M1043 HMMWV Armament Air [7023lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = true},
  ["APC M1043 HMMWV Armament Skid [6912lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = false},
  ["SAM Avenger M1097 Air [7200lb]"] = {['name'] = "M1097 Avenger", ['container'] = true},
  ["SAM Avenger M1097 Skid [7090lb]"] = {['name'] = "M1097 Avenger", ['container'] = false},
  ["APC Cobra Air [10912lb]"] = {['name'] = "Cobra", ['container'] = true},
  ["APC Cobra Skid [10802lb]"] = {['name'] = "Cobra", ['container'] = false},
  ["APC M113 Air [21624lb]"] = {['name'] = "M-113", ['container'] = true},
  ["APC M113 Skid [21494lb]"] = {['name'] = "M-113", ['container'] = false},
  ["Tanker M978 HEMTT [34000lb]"] = {['name'] = "M978 HEMTT Tanker", ['container'] = false},
  ["HEMTT TFFT [34400lb]"] = {['name'] = "HEMTT TFFT", ['container'] = false},
  ["SPG M1128 Stryker MGS [33036lb]"] = {['name'] = "M1128 Stryker MGS", ['container'] = false},
  ["AAA Vulcan M163 Air [21666lb]"] = {['name'] = "Vulcan", ['container'] = true},
  ["AAA Vulcan M163 Skid [21577lb]"] = {['name'] = "Vulcan", ['container'] = false},
  ["APC M1126 Stryker ICV [29542lb]"] = {['name'] = "M1126 Stryker ICV", ['container'] = false},
  ["ATGM M1134 Stryker [30337lb]"] = {['name'] = "M1134 Stryker ATGM", ['container'] = false},
  ["APC LAV-25 Air [22520lb]"] = {['name'] = "LAV-25", ['container'] = true},
  ["APC LAV-25 Skid [22514lb]"] = {['name'] = "LAV-25", ['container'] = false},
  ["M1025 HMMWV Air [6160lb]"] = {['name'] = "Hummer", ['container'] = true},
  ["M1025 HMMWV Skid [6050lb]"] = {['name'] = "Hummer", ['container'] = false},
  ["IFV M2A2 Bradley [34720lb]"] = {['name'] = "M-2 Bradley", ['container'] = false},
  ["IFV MCV-80 [34720lb]"] = {['name'] = "MCV-80", ['container'] = false},
  ["IFV BMP-1 [23232lb]"] = {['name'] = "BMP-1", ['container'] = false},
  ["IFV BMP-2 [25168lb]"] = {['name'] = "BMP-2", ['container'] = false},
  ["IFV BMP-3 [32912lb]"] = {['name'] = "BMP-3", ['container'] = false},
  ["ARV BRDM-2 Air [12320lb]"] = {['name'] = "BRDM-2", ['container'] = true},
  ["ARV BRDM-2 Skid [12210lb]"] = {['name'] = "BRDM-2", ['container'] = false},
  ["APC BTR-80 Air [23936lb]"] = {['name'] = "BTR-80", ['container'] = true},
  ["APC BTR-80 Skid [23826lb]"] = {['name'] = "BTR-80", ['container'] = false},
  ["APC BTR-82A Air [24998lb]"] = {['name'] = "BTR-82A", ['container'] = true},
  ["APC BTR-82A Skid [24888lb]"] = {['name'] = "BTR-82A", ['container'] = false},
  ["SAM ROLAND ADS [34720lb]"] = {['name'] = "Roland Radar", ['container'] = false},
  ["SAM ROLAND LN [34720b]"] = {['name'] = "Roland ADS", ['container'] = false},
  ["SAM SA-13 STRELA [21624lb]"] = {['name'] = "Strela-10M3", ['container'] = false},
  ["AAA ZSU-23-4 Shilka [32912lb]"] = {['name'] = "ZSU-23-4 Shilka", ['container'] = false},
  ["SAM SA-19 Tunguska 2S6 [34720lb]"] = {['name'] = "2S6 Tunguska", ['container'] = false},
  ["Transport UAZ-469 Air [3747lb]"] = {['name'] = "UAZ-469", ['container'] = true},
  ["Transport UAZ-469 Skid [3630lb]"] = {['name'] = "UAZ-469", ['container'] = false},
  ["AAA GEPARD [34720lb]"] = {['name'] = "Gepard", ['container'] = false},
  ["SAM CHAPARRAL Air [21624lb]"] = {['name'] = "M48 Chaparral", ['container'] = true},
  ["SAM CHAPARRAL Skid [21516lb]"] = {['name'] = "M48 Chaparral", ['container'] = false},
  ["SAM LINEBACKER [34720lb]"] = {['name'] = "M6 Linebacker", ['container'] = false},
  ["Transport URAL-375 [14815lb]"] = {['name'] = "Ural-375", ['container'] = false},
  ["Transport M818 [16000lb]"] = {['name'] = "M 818", ['container'] = false},
  ["IFV MARDER [34720lb]"] = {['name'] = "Marder", ['container'] = false},
  ["Transport Tigr Air [15900lb]"] = {['name'] = "Tigr_233036", ['container'] = true},
  ["Transport Tigr Skid [15730lb]"] = {['name'] = "Tigr_233036", ['container'] = false},
  ["IFV TPZ FUCH [33440lb]"] = {['name'] = "TPZ", ['container'] = false},
  ["IFV BMD-1 Air [18040lb]"] = {['name'] = "BMD-1", ['container'] = true},
  ["IFV BMD-1 Skid [17930lb]"] = {['name'] = "BMD-1", ['container'] = false},
  ["IFV BTR-D Air [18040lb]"] = {['name'] = "BTR_D", ['container'] = true},
  ["IFV BTR-D Skid [17930lb]"] = {['name'] = "BTR_D", ['container'] = false},
  ["EWR SBORKA Air [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = true},
  ["EWR SBORKA Skid [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = false},
  ["ART 2S9 NONA Air [19140lb]"] = {['name'] = "SAU 2-C9", ['container'] = true},
  ["ART 2S9 NONA Skid [19030lb]"] = {['name'] = "SAU 2-C9", ['container'] = false},
  ["ART GVOZDIKA [34720lb]"] = {['name'] = "SAU Gvozdika", ['container'] = false},
  ["APC MTLB Air [26400lb]"] = {['name'] = "MTLB", ['container'] = true},
  ["APC MTLB Skid [26290lb]"] = {['name'] = "MTLB", ['container'] = false},
}

--- Cargo Object
-- @type CTLD_HERCULES.CargoObject
-- @field #number Cargo_Drop_Direction
-- @field #table Cargo_Contents
-- @field #string Cargo_Type_name
-- @field #boolean Container_Enclosed
-- @field #boolean ParatrooperGroupSpawn
-- @field #number Cargo_Country
-- @field #boolean offload_cargo
-- @field #boolean all_cargo_survive_to_the_ground
-- @field #boolean all_cargo_gets_destroyed
-- @field #boolean destroy_cargo_dropped_without_parachute
-- @field Core.Timer#TIMER scheduleFunctionID

--- [User] Instantiate a new object
-- @param #CTLD_HERCULES self
-- @param #string Coalition Coalition side, "red", "blue" or "neutral"
-- @param #string Alias Name of this instance
-- @param Ops.CTLD#CTLD CtldObject CTLD instance to link into
-- @return #CTLD_HERCULES self
-- @usage
-- Integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance:
--            
--            my_ctld.enableFixedWing = false -- avoid dual loading via CTLD F10 and F8 ground crew
--            local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld)
--            
-- You also need: 
-- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate). 
-- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable. 
-- There's a **quick check output in the `dcs.log`** which tells you what's there and what not.
-- E.g.:
--            ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING)
--            ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING)
--            ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING)
--            ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK)
--            
-- Expected template names are the ones in the rounded brackets.
-- 
-- ### HINTS
-- 
-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does
-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD.
-- **Do not use** the **splash damage** script together with this, your cargo will just explode when reaching the ground!
-- 
-- ### Airdrops
-- 
-- There are two ways of airdropping:   
-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu)
-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu)
-- 
-- ### General
-- 
-- Use either this method to integrate the Hercules **or** the one from the "normal" CTLD. Never both!
function CTLD_HERCULES:New(Coalition, Alias, CtldObject)
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #CTLD_HERCULES
  
  --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in CTLD!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end
  
  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="UNHCR"  
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="Red CTLD Hercules"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="Blue CTLD Hercules"
      end
    end
  end
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalitiontxt)
  
  self.infantrytemplate = "Infantry" -- template for a group of 10 paratroopers
  self.CTLD = CtldObject -- Ops.CTLD#CTLD
  
  self.verbose = true
  
  self.j = 0
  self.carrierGroups = {}
  self.Cargo = {}
  self.ParatrooperCount = {}
  
  self.ObjectTracker = {}
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")

  self:HandleEvent(EVENTS.Shot, self._HandleShot)
  
  self:I(self.lid .. "Started")
  
  self:CheckTemplates()
    
  return self
end

--- [Internal] Function to check availability of templates
-- @param #CTLD_HERCULES self
-- @return #CTLD_HERCULES self 
function CTLD_HERCULES:CheckTemplates()
  self:T(self.lid .. 'CheckTemplates')
  -- inject Paratroopers
  self.Types["Paratroopers 10"] = {
    name = self.infantrytemplate,
    container = false,
    available = false,
  }
  local missing = {}
  local nomissing = 0
  local found = {}
  local nofound = 0
  
  -- list of groundcrew loadables
  for _index,_tab in pairs (self.Types) do
    local outcometxt = "MISSING"
    if _DATABASE.Templates.Groups[_tab.name] then
      outcometxt = "OK"
      self.Types[_index].available= true
      found[_tab.name] = true
    else
      self.Types[_index].available = false
      missing[_tab.name] = true
    end
    if self.verbose then
      self:I(string.format(self.lid .. "Checking template for %s (%s) ... %s", _index,_tab.name,outcometxt))
    end
  end
  for _,_name in pairs(found) do
    nofound = nofound + 1
  end
  for _,_name in pairs(missing) do
    nomissing = nomissing + 1
  end
  self:I(string.format(self.lid .. "Template Check Summary: Found %d, Missing %d, Total %d",nofound,nomissing,nofound+nomissing))
  return self
end

--- [Internal] Function to spawn a soldier group of 10 units
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @param #string Cargo_Type_name
-- @param #number CargoHeading
-- @param #number Cargo_Country
-- @param #number GroupSpacing
-- @return #CTLD_HERCULES self 
function CTLD_HERCULES:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, Cargo_Country, GroupSpacing)
  --- TODO: Rework into Moose Spawns
  self:T(self.lid .. 'Soldier_SpawnGroup')
  self:T(Cargo_Drop_Position)
   -- create a matching #CTLD_CARGO type
   local InjectTroopsType = CTLD_CARGO:New(nil,self.infantrytemplate,{self.infantrytemplate},CTLD_CARGO.Enum.TROOPS,true,true,10,nil,false,80)
   -- get a #ZONE object
   local position = Cargo_Drop_Position:GetVec2()
   local dropzone = ZONE_RADIUS:New("Infantry " .. math.random(1,10000),position,100)
   -- and go:
   self.CTLD:InjectTroops(dropzone,InjectTroopsType)
  return self
end

--- [Internal] Function to spawn a group
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @param #string Cargo_Type_name
-- @param #number CargoHeading
-- @param #number Cargo_Country
-- @return #CTLD_HERCULES self 
function CTLD_HERCULES:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, Cargo_Country)
  --- TODO: Rework into Moose Spawns
  self:T(self.lid .. "Cargo_SpawnGroup")
  self:T(Cargo_Type_name)   
  if Cargo_Type_name ~= 'Container red 1' then
   -- create a matching #CTLD_CARGO type
   local InjectVehicleType = CTLD_CARGO:New(nil,Cargo_Type_name,{Cargo_Type_name},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000)
   -- get a #ZONE object
   local position = Cargo_Drop_Position:GetVec2()
   local dropzone = ZONE_RADIUS:New("Vehicle " .. math.random(1,10000),position,100)
   -- and go:
   self.CTLD:InjectVehicles(dropzone,InjectVehicleType)
  end
  return self
end

--- [Internal] Function to spawn static cargo
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @param #string Cargo_Type_name
-- @param #number CargoHeading
-- @param #boolean dead
-- @param #number Cargo_Country
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, dead, Cargo_Country)
  --- TODO: Rework into Moose Static Spawns
  self:T(self.lid .. "Cargo_SpawnStatic")
  self:T("Static " .. Cargo_Type_name .. " Dead " .. tostring(dead))
  local position = Cargo_Drop_Position:GetVec2()
  local Zone = ZONE_RADIUS:New("Cargo Static " .. math.random(1,10000),position,100)
  if not dead then
    local injectstatic = CTLD_CARGO:New(nil,"Cargo Static Group "..math.random(1,10000),"iso_container",CTLD_CARGO.Enum.STATIC,true,false,1,nil,true,4500,1) 
    self.CTLD:InjectStatics(Zone,injectstatic,true,true)
  end
  return self
end

--- [Internal] Function to spawn cargo by type at position
-- @param #CTLD_HERCULES self
-- @param #string Cargo_Type_name
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_SpawnDroppedAsCargo(_name, _pos)
  local theCargo = self.CTLD:_FindCratesCargoObject(_name) -- #CTLD_CARGO
  if theCargo then
    self.CTLD.CrateCounter = self.CTLD.CrateCounter + 1
    local CCat, CType, CShape = theCargo:GetStaticTypeAndShape()
    local basetype = CType or self.CTLD.basetype or "container_cargo"
    CCat = CCat or "Cargos"
    local theStatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)    
    :InitCargoMass(theCargo.PerCrateMass)
    :InitCargo(self.CTLD.enableslingload)
    :InitCoordinate(_pos)
    if CShape then
      theStatic:InitShape(CShape)
    end
    theStatic:Spawn(270,_name .. "-Container-".. math.random(1,100000))
  
    self.CTLD.Spawned_Crates[self.CTLD.CrateCounter] = theStatic
    local newCargo = CTLD_CARGO:New(self.CTLD.CargoCounter, theCargo.Name, theCargo.Templates, theCargo.CargoType, true, false, theCargo.CratesNeeded, self.CTLD.Spawned_Crates[self.CTLD.CrateCounter], true, theCargo.PerCrateMass, nil, theCargo.Subcategory)
    local map=theCargo:GetStaticResourceMap()
    newCargo:SetStaticResourceMap(map)
    table.insert(self.CTLD.Spawned_Cargo, newCargo)

    newCargo:SetWasDropped(true)
    newCargo:SetHasMoved(true)
  end
  return self
end

--- [Internal] Spawn cargo objects
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param #number Cargo_Drop_Direction
-- @param Core.Point#COORDINATE Cargo_Content_position
-- @param #string Cargo_Type_name
-- @param #boolean Cargo_over_water
-- @param #boolean Container_Enclosed
-- @param #boolean ParatrooperGroupSpawn
-- @param #boolean offload_cargo
-- @param #boolean all_cargo_survive_to_the_ground
-- @param #boolean all_cargo_gets_destroyed
-- @param #boolean destroy_cargo_dropped_without_parachute
-- @param #number Cargo_Country
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_SpawnObjects(Cargo_Drop_initiator,Cargo_Drop_Direction, Cargo_Content_position, Cargo_Type_name, Cargo_over_water, Container_Enclosed, ParatrooperGroupSpawn, offload_cargo, all_cargo_survive_to_the_ground, all_cargo_gets_destroyed, destroy_cargo_dropped_without_parachute, Cargo_Country)
  self:T(self.lid .. 'Cargo_SpawnObjects')
  
  local CargoHeading = self.CargoHeading
  
  if offload_cargo == true or ParatrooperGroupSpawn == true then  
    if ParatrooperGroupSpawn == true then
      self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 10)
    else
      self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country)
    end
  else
    if all_cargo_gets_destroyed == true or Cargo_over_water == true then

    else
      if all_cargo_survive_to_the_ground == true then
        if ParatrooperGroupSpawn == true then
          self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country)
        else
          self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country)
        end
        if Container_Enclosed == true then
          if ParatrooperGroupSpawn == false then
            self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, "Hercules_Container_Parachute_Static", CargoHeading, false, Cargo_Country)
          end
        end
      end
      if destroy_cargo_dropped_without_parachute == true then
        if Container_Enclosed == true then
          if ParatrooperGroupSpawn == true then
            self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 0)
          else
            if self.CTLD.dropAsCargoCrate then
              self:Cargo_SpawnDroppedAsCargo(Cargo_Type_name, Cargo_Content_position)
            else
              self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country)
              self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, "Hercules_Container_Parachute_Static", CargoHeading, false, Cargo_Country)
            end
          end
        else
          self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country)
        end
      end
    end
  end
  return self
end

--- [Internal] Function to calculate object height
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP group The group for which to calculate the height
-- @return #number height over ground
function CTLD_HERCULES:Calculate_Object_Height_AGL(group)
  self:T(self.lid .. "Calculate_Object_Height_AGL")
  if group.ClassName and group.ClassName == "GROUP" then
    local gcoord = group:GetCoordinate()
    local height = group:GetHeight()
    local lheight = gcoord:GetLandHeight()
    self:T(self.lid .. "Height " .. height - lheight)
    return height - lheight
  else
    -- DCS object
    if group:isExist() then
      local dcsposition = group:getPosition().p
      local dcsvec2 = {x = dcsposition.x, y = dcsposition.z} -- Vec2
      local height = math.floor(group:getPosition().p.y - land.getHeight(dcsvec2))
      self.ObjectTracker[group.id_] = dcsposition --  Vec3
      self:T(self.lid .. "Height " .. height)
      return height
    else
      return 0
    end
  end
end

--- [Internal] Function to check surface type
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP group The group for which to calculate the height
-- @return #number height over ground
function CTLD_HERCULES:Check_SurfaceType(object)
  self:T(self.lid .. "Check_SurfaceType")
   -- LAND,--1 SHALLOW_WATER,--2 WATER,--3 ROAD,--4 RUNWAY--5
  if object:isExist() then
   return land.getSurfaceType({x = object:getPosition().p.x, y = object:getPosition().p.z})
  else
   return 1
  end
end

--- [Internal] Function to track cargo objects
-- @param #CTLD_HERCULES self
-- @param #CTLD_HERCULES.CargoObject cargo
-- @param Wrapper.Group#GROUP initiator
-- @return #number height over ground
function CTLD_HERCULES:Cargo_Track(cargo, initiator)
  self:T(self.lid .. "Cargo_Track")
  local Cargo_Drop_initiator = initiator
  if cargo.Cargo_Contents ~= nil then
    if self:Calculate_Object_Height_AGL(cargo.Cargo_Contents) < 10 then --pallet less than 5m above ground before spawning
      if self:Check_SurfaceType(cargo.Cargo_Contents) == 2 or self:Check_SurfaceType(cargo.Cargo_Contents) == 3 then
        cargo.Cargo_over_water = true--pallets gets destroyed in water
      end
      local dcsvec3 = self.ObjectTracker[cargo.Cargo_Contents.id_] or initiator:GetVec3() -- last known position
      self:T("SPAWNPOSITION: ")
      self:T({dcsvec3})
      local Vec2 = {
         x=dcsvec3.x,
         y=dcsvec3.z,
      }
      local vec3 = COORDINATE:NewFromVec2(Vec2)
      self.ObjectTracker[cargo.Cargo_Contents.id_] =  nil
      self:Cargo_SpawnObjects(Cargo_Drop_initiator,cargo.Cargo_Drop_Direction, vec3, cargo.Cargo_Type_name, cargo.Cargo_over_water, cargo.Container_Enclosed, cargo.ParatrooperGroupSpawn, cargo.offload_cargo, cargo.all_cargo_survive_to_the_ground, cargo.all_cargo_gets_destroyed, cargo.destroy_cargo_dropped_without_parachute, cargo.Cargo_Country)
      if cargo.Cargo_Contents:isExist() then
       cargo.Cargo_Contents:destroy()--remove pallet+parachute before hitting ground and replace with Cargo_SpawnContents
      end
      --timer.removeFunction(cargo.scheduleFunctionID)
      cargo.scheduleFunctionID:Stop()
      cargo = {}
    end
  end
  return self
end

--- [Internal] Function to calc north correction
-- @param #CTLD_HERCULES self
-- @param Core.Point#POINT_Vec3 point Position Vec3
-- @return #number north correction
function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_NorthCorrection(point)
  self:T(self.lid .. "Calculate_Cargo_Drop_initiator_NorthCorrection")
  if not point.z then --Vec2; convert to Vec3
    point.z = point.y
    point.y = 0
  end
  local lat, lon = coord.LOtoLL(point)
  local north_posit = coord.LLtoLO(lat + 1, lon)
  return math.atan2(north_posit.z - point.z, north_posit.x - point.x)
end

--- [Internal] Function to calc initiator heading
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @return #number north corrected heading
function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_Heading(Cargo_Drop_initiator)
  self:T(self.lid .. "Calculate_Cargo_Drop_initiator_Heading") 
  local Heading = Cargo_Drop_initiator:GetHeading()
  Heading = Heading + self:Calculate_Cargo_Drop_initiator_NorthCorrection(Cargo_Drop_initiator:GetVec3())
  if Heading < 0 then
    Heading = Heading + (2 * math.pi)-- put heading in range of 0 to 2*pi
  end
  return Heading + 0.06 -- rad
end

--- [Internal] Function to initialize dropped cargo
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Initiator
-- @param #table Cargo_Contents Table 'weapon' from event data
-- @param #string Cargo_Type_name Name of this cargo
-- @param #boolean Container_Enclosed Is container?
-- @param #boolean SoldierGroup Is soldier group?
-- @param #boolean ParatrooperGroupSpawnInit Is paratroopers?
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_Initialize(Initiator, Cargo_Contents, Cargo_Type_name, Container_Enclosed, SoldierGroup, ParatrooperGroupSpawnInit)
  self:T(self.lid .. "Cargo_Initialize") 
    local Cargo_Drop_initiator = Initiator:GetName()
    if Cargo_Drop_initiator ~= nil then
      if ParatrooperGroupSpawnInit == true then
         self:T("Paratrooper Drop")
         -- Paratroopers
        if not self.ParatrooperCount[Cargo_Drop_initiator] then
         self.ParatrooperCount[Cargo_Drop_initiator] = 1
        else
         self.ParatrooperCount[Cargo_Drop_initiator] = self.ParatrooperCount[Cargo_Drop_initiator] + 1
        end
        
        local Paratroopers = self.ParatrooperCount[Cargo_Drop_initiator]
        
        self:T("Paratrooper Drop Number " .. self.ParatrooperCount[Cargo_Drop_initiator])
        
        local SpawnParas = false
        
        if math.fmod(Paratroopers,10) == 0 then
          SpawnParas = true
        end
        
        self.j = self.j + 1
        self.Cargo[self.j] = {}
        self.Cargo[self.j].Cargo_Drop_Direction = self:Calculate_Cargo_Drop_initiator_Heading(Initiator)
        self.Cargo[self.j].Cargo_Contents = Cargo_Contents
        self.Cargo[self.j].Cargo_Type_name = Cargo_Type_name
        self.Cargo[self.j].Container_Enclosed = Container_Enclosed
        self.Cargo[self.j].ParatrooperGroupSpawn = SpawnParas
        self.Cargo[self.j].Cargo_Country = Initiator:GetCountry()

        if self:Calculate_Object_Height_AGL(Initiator) < 5.0 then --aircraft on ground
          self.Cargo[self.j].offload_cargo = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 10.0 then --aircraft less than 10m above ground
          self.Cargo[self.j].all_cargo_survive_to_the_ground = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 100.0 then --aircraft more than 10m but less than 100m above ground
          self.Cargo[self.j].all_cargo_gets_destroyed = true
        else
         self.Cargo[self.j].all_cargo_gets_destroyed = false  
        end

        local timer = TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator)
        self.Cargo[self.j].scheduleFunctionID  = timer
        timer:Start(1,1,600)  
        
      else
       -- no paras
        self.j = self.j + 1
        self.Cargo[self.j] = {}
        self.Cargo[self.j].Cargo_Drop_Direction = self:Calculate_Cargo_Drop_initiator_Heading(Initiator)
        self.Cargo[self.j].Cargo_Contents = Cargo_Contents
        self.Cargo[self.j].Cargo_Type_name = Cargo_Type_name
        self.Cargo[self.j].Container_Enclosed = Container_Enclosed
        self.Cargo[self.j].ParatrooperGroupSpawn = false
        self.Cargo[self.j].Cargo_Country = Initiator:GetCountry()

        if self:Calculate_Object_Height_AGL(Initiator) < 5.0 then--aircraft on ground
          self.Cargo[self.j].offload_cargo = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 10.0 then--aircraft less than 10m above ground
          self.Cargo[self.j].all_cargo_survive_to_the_ground = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 100.0 then--aircraft more than 10m but less than 100m above ground
          self.Cargo[self.j].all_cargo_gets_destroyed = true
        else
          self.Cargo[self.j].destroy_cargo_dropped_without_parachute = true --aircraft more than 100m above ground
        end
        
        local timer = TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator)
        self.Cargo[self.j].scheduleFunctionID  = timer
        timer:Start(1,1,600)
      end
    end
  return self 
end

--- [Internal] Function to change cargotype per group (Wrench)
-- @param #CTLD_HERCULES self
-- @param #number key Carrier key id
-- @param #string cargoType Type of cargo
-- @param #number cargoNum Number of cargo objects
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:SetType(key,cargoType,cargoNum)
  self:T(self.lid .. "SetType") 
  self.carrierGroups[key]['cargoType'] = cargoType
  self.carrierGroups[key]['cargoNum'] = cargoNum
  return self
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- EventHandlers
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] Function to capture SHOT event
-- @param #CTLD_HERCULES self
-- @param Core.Event#EVENTDATA Cargo_Drop_Event The event data
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:_HandleShot(Cargo_Drop_Event)
  self:T(self.lid .. "Shot Event ID:" .. Cargo_Drop_Event.id) 
  if Cargo_Drop_Event.id == EVENTS.Shot then
    
    local GT_Name = ""
    local SoldierGroup = false
    local ParatrooperGroupSpawnInit = false
    
    local GT_DisplayName = Weapon.getDesc(Cargo_Drop_Event.weapon).typeName:sub(15, -1)--Remove "weapons.bombs." from string
    self:T(string.format("%sCargo_Drop_Event: %s", self.lid, Weapon.getDesc(Cargo_Drop_Event.weapon).typeName))
 
    if (GT_DisplayName == "Squad 30 x Soldier [7950lb]") then
      self:Cargo_Initialize(Cargo_Drop_Event.IniGroup, Cargo_Drop_Event.weapon, "Soldier M4 GRG", false, true, true)
    end
 
    if self.Types[GT_DisplayName] then
      local GT_Name = self.Types[GT_DisplayName]['name']
    local Cargo_Container_Enclosed = self.Types[GT_DisplayName]['container']
        self:Cargo_Initialize(Cargo_Drop_Event.IniGroup, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed)
      end
  end
  return self
end

--- [Internal] Function to capture BIRTH event
-- @param #CTLD_HERCULES self
-- @param Core.Event#EVENTDATA event The event data
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:_HandleBirth(event)
  -- not sure what this is needed for? I think this for setting generic crates "content" setting.
  self:T(self.lid .. "Birth Event ID:" .. event.id)
  return self
end

end

-------------------------------------------------------------------
-- End Ops.CTLD.lua
-------------------------------------------------------------------
