--- **Ops** - Combat Search and Rescue.
--
-- ===
--
-- **CSAR** - MOOSE based Helicopter CSAR Operations.
--
-- ===
--
-- ## Missions:--- **Ops** -- Combat Search and Rescue.
--
-- ===
--
-- **CSAR** - MOOSE based Helicopter CSAR Operations.
--
-- ===
--
-- ## Missions:
--
-- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/CSAR)
--
-- ===
--
-- **Main Features:**
--
--    * MOOSE-based Helicopter CSAR Operations for Players.
--
-- ===
--
-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), The Chosen One (Persistence)
-- @module Ops.CSAR
-- @image OPS_CSAR.jpg

---
-- Last Update Oct 2025

-------------------------------------------------------------------------
--- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM
-- @type CSAR
-- @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 Core.Set#SET_GROUP allheligroupset Set of CSAR heli groups.
-- @field Core.Set#SET_GROUP UserSetGroup Set of CSAR heli groups as designed by the mission designer (if any set).
-- @extends Core.Fsm#FSM

--- *Combat search and rescue (CSAR) are search and rescue operations that are carried out during war that are within or near combat zones.* (Wikipedia)
--
-- ===
--
-- # CSAR Concept
--
--  * MOOSE-based Helicopter CSAR Operations for Players.
--  * Object oriented refactoring of Ciribob\'s fantastic CSAR script.
--  * No need for extra MIST loading.
--  * Additional events to tailor your mission.
--  * Optional SpawnCASEVAC to create casualties without beacon (e.g. handling dead ground vehicles and create CASVAC requests).
--
-- ## 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 a late-activated single infantry unit as template in the mission editor and name it e.g. "Downed Pilot".
--
-- Example sound files are here: [Moose Sound](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/CTLD%20CSAR)
--
-- ## 1. Basic Setup
--
-- A basic setup example is the following:
--
--        -- Instantiate and start a CSAR for the blue side, with template "Downed Pilot" and alias "Luftrettung"
--        local my_csar = CSAR:New(coalition.side.BLUE,"Downed Pilot","Luftrettung")
--        -- options
--        my_csar.immortalcrew = true -- downed pilot spawn is immortal
--        my_csar.invisiblecrew = false -- downed pilot spawn is visible
--        -- start the FSM
--        my_csar:__Start(5)
--
-- ## 2. Options
--
-- The following options are available (with their defaults). Only set the ones you want changed:
--
--         mycsar.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms.
--         mycsar.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only!
--         mycsar.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
--         mycsar.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near.
--         mycsar.autosmokedistance = 1000 -- distance for autosmoke
--         mycsar.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates.
--         mycsar.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well.
--         mycsar.enableForAI = false -- set to false to disable AI pilots from being rescued.
--         mycsar.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to mycsar.extractDistance in meters.
--         mycsar.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter.
--         mycsar.immortalcrew = true -- Set to true to make wounded crew immortal.
--         mycsar.invisiblecrew = false -- Set to true to make wounded crew insvisible.
--         mycsar.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters.
--         mycsar.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. Will also try to add ZONE and STATIC objects with this prefix once at startup.
--         mycsar.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined.
--         mycsar.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages.
--         mycsar.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons.
--         mycsar.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue.
--         mycsar.useprefix = true  -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below.
--         mycsar.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor!
--         mycsar.verbose = 0 -- set to > 1 for stats output for debugging.
--         -- limit amount of downed pilots spawned by **ejection** events
--         mycsar.limitmaxdownedpilots = true
--         mycsar.maxdownedpilots = 10
--         -- allow to set far/near distance for approach and optionally pilot must open doors
--         mycsar.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters
--         mycsar.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters
--         mycsar.pilotmustopendoors = false -- switch to true to enable check of open doors
--         mycsar.suppressmessages = false -- switch off all messaging if you want to do your own
--         mycsar.rescuehoverheight = 20 -- max height for a hovering rescue in meters
--         mycsar.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters
--         -- Country codes for spawned pilots
--         mycsar.countryblue= country.id.USA
--         mycsar.countryred = country.id.RUSSIA
--         mycsar.countryneutral = country.id.UN_PEACEKEEPERS
--         mycsar.topmenuname = "CSAR" -- set the menu entry name
--         mycsar.ADFRadioPwr = 1000 -- ADF Beacons sending with 1KW as default
--         mycsar.PilotWeight = 80 --  Loaded pilots weigh 80kgs each
--         mycsar.AllowIRStrobe = false -- Allow a menu item to request an IR strobe to find a downed pilot at night (requires NVGs to see it).
--         mycsar.IRStrobeRuntime = 300 -- If an IR Strobe is activated, it runs for 300 seconds (5 mins).
--
-- ## 2.1 Create own SET_GROUP to manage CTLD Pilot groups
--
--         -- Parameter: Set The SET_GROUP object created by the mission designer/user to represent the CSAR pilot groups.
--         -- Needs to be set before starting the CSAR instance.
--         local myset = SET_GROUP:New():FilterPrefixes("Helikopter"):FilterCoalitions("red"):FilterStart()
--         mycsar:SetOwnSetPilotGroups(myset)
--
-- ## 2.2 SRS Features and Other Features
--
--       mycsar.useSRS = false -- Set true to use FF\'s SRS integration
--       mycsar.SRSPath = "C:\\Progra~1\\DCS-SimpleRadio-Standalone\\ExternalAudio\\" -- adjust your own path in your SRS installation -- server(!)
--       mycsar.SRSchannel = 300 -- radio channel
--       mycsar.SRSModulation = radio.modulation.AM -- modulation
--       mycsar.SRSport = 5002  -- and SRS Server port
--       mycsar.SRSCulture = "en-GB" -- SRS voice culture
--       mycsar.SRSVoice = nil -- SRS voice for downed pilot, relevant for Google TTS
--       mycsar.SRSGPathToCredentials = nil -- Path to your Google credentials json file, set this if you want to use Google TTS
--       mycsar.SRSVolume = 1 -- Volume, between 0 and 1
--       mycsar.SRSGender = "male" -- male or female voice
--       mycsar.CSARVoice = MSRS.Voices.Google.Standard.en_US_Standard_A -- SRS voice for CSAR Controller, relevant for Google TTS
--       mycsar.CSARVoiceMS = MSRS.Voices.Microsoft.Hedda -- SRS voice for CSAR Controller, relevant for MS Desktop TTS
--       mycsar.coordinate -- Coordinate from which CSAR TTS is sending. Defaults to a random MASH object position
--       --
--       mycsar.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection. Requires mycsar.enableForAI to be set to true. --shagrat
--       mycsar.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases.
--       mycsar.allowbronco = false  -- set to true to use the Bronco mod as a CSAR plane
--       mycsar.CreateRadioBeacons = true -- set to false to disallow creating ADF radio beacons.
--
-- ## 3. Results
--
-- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object:
--
--        mycsar.rescues -- number of successful landings *with* saved pilots
--        mycsar.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players)
--
-- ## 4. Events
--
--  The class comes with a number of FSM-based events that missions designers can use to shape their mission.
--  These are:
--
-- ### 4.1. PilotDown.
--
--      The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event:
--
--          function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text)
--            ... your code here ...
--          end
--
-- ### 4.2. Approach.
--
--      A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event:
--
--          function my_csar:OnAfterApproach(from, event, to, heliname, groupname)
--            ... your code here ...
--          end
--
-- ### 4.3. Boarded.
--
--      The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event:
--
--          function my_csar:OnAfterBoarded(from, event, to, heliname, groupname, description)
--            ... your code here ...
--          end
--
-- ### 4.4. Returning.
--
--       The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event:
--
--          function my_csar:OnAfterReturning(from, event, to, heliname, groupname)
--            ... your code here ...
--          end
--
-- ### 4.5. Rescued.
--
--      The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event:
--
--          function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved)
--            ... your code here ...
--          end
--
-- ## 5. Spawn downed pilots at location to be picked up.
--
--      If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so:
--
--        -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition
--        my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true )
--
--      --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat
--      my_csar:SpawnCASEVAC(Point, coalition.side.BLUE)
--
-- ## 6. Save and load downed pilots - Persistance
--
-- You can save and later load back downed pilots 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:
--
--              mycsar.enableLoadSave = true -- allow auto-saving and loading of files
--              mycsar.saveinterval = 600 -- save every 10 minutes
--              mycsar.filename = "missionsave.csv" -- example filename
--              mycsar.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path
--
--  Then use an initial load at the beginning of your mission:
--
--            mycsar:__Load(10)
--
--  **Caveat:**
-- Dropped troop noMessage and forcedesc parameters aren't saved.
--
-- @field #CSAR
CSAR = {
  ClassName       = "CSAR",
  verbose         =     0,
  lid             =   "",
  coalition       = 1,
  coalitiontxt    = "blue",
  FreeVHFFrequencies = {},
  UsedVHFFrequencies = {},
  takenOff = {},
  csarUnits = {},  -- table of unit names
  downedPilots = {},
  -- = {},
  landedStatus = {},
  addedTo = {},
  woundedGroups = {}, -- contains the new group of units
  inTransitGroups = {}, -- contain a table for each SAR with all units he has with the original names
  smokeMarkers = {}, -- tracks smoke markers for groups
  heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible
  heliCloseMessage = {}, -- tracks heli close message  ie heli < 500m distance
  max_units = 6, -- number of pilots that can be carried
  hoverStatus = {}, -- tracks status of a helis hover above a downed pilot
  pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for
  pilotLives = {}, -- tracks how many lives a pilot has
  useprefix    = true,  -- Use the Prefixed defined below, Requires Unit have the Prefix defined below
  csarPrefix = {},
  template = nil,
  mash = {},
  smokecolor = 4,
  rescues = 0,
  rescuedpilots = 0,
  limitmaxdownedpilots = true,
  maxdownedpilots = 10,
  useFIFOLimitReplacement = false, -- If true, it will remove the oldest downed pilot when a new one is added, if the limit is reached.
  allheligroupset = nil,
  topmenuname = "CSAR",
  ADFRadioPwr = 1000,
  PilotWeight = 80,
  CreateRadioBeacons = true,
  UserSetGroup = nil,
  AllowIRStrobe = false,
  IRStrobeRuntime = 300,
}

--- Downed pilots info.
-- @type CSAR.DownedPilot
-- @field #number index Pilot index.
-- @field #string name Name of the spawned group.
-- @field #number side Coalition.
-- @field #string originalUnit Name of the original unit.
-- @field #string desc Description.
-- @field #string typename Typename of Unit.
-- @field #number frequency Frequency of the NDB.
-- @field #string player Player name if applicable.
-- @field Wrapper.Group#GROUP group Spawned group object.
-- @field #number timestamp Timestamp for approach process.
-- @field #boolean alive Group is alive or dead/rescued.
-- @field #boolean wetfeet Group is spawned over (deep) water.
-- @field #string BeaconName Name of radio beacon - if any.

--- All slot / Limit settings
-- @type CSAR.AircraftType
-- @field #string typename Unit type name.
CSAR.AircraftType = {} -- Type and limit
CSAR.AircraftType["SA342Mistral"] = 2
CSAR.AircraftType["SA342Minigun"] = 2
CSAR.AircraftType["SA342L"] = 4
CSAR.AircraftType["SA342M"] = 4
CSAR.AircraftType["UH-1H"] = 8
CSAR.AircraftType["Mi-8MTV2"] = 12
CSAR.AircraftType["Mi-8MT"] = 12
CSAR.AircraftType["Mi-24P"] = 8
CSAR.AircraftType["Mi-24V"] = 8
CSAR.AircraftType["Bell-47"] = 2
CSAR.AircraftType["UH-60L"] = 10
CSAR.AircraftType["UH-60L_DAP"] = 2
CSAR.AircraftType["AH-64D_BLK_II"] = 2
CSAR.AircraftType["Bronco-OV-10A"] = 2
CSAR.AircraftType["MH-60R"] = 10
CSAR.AircraftType["OH-6A"] = 2
CSAR.AircraftType["OH58D"] = 2
CSAR.AircraftType["CH-47Fbl1"] = 31

--- CSAR class version.
-- @field #string version
CSAR.version="1.0.34"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: SRS Integration (to be tested)
-- TODO: Maybe - add option to smoke/flare closest MASH
-- DONE: shagrat Add cargoWeight to helicopter when pilot boarded
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new CSAR object and start the FSM.
-- @param #CSAR self
-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral".
-- @param #string Template Name of the late activated infantry unit standing in for the downed pilot.
-- @param #string Alias An *optional* alias how this object is called in the logs etc.
-- @return #CSAR self
function CSAR:New(Coalition, Template, Alias)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #CSAR

  BASE:T({Coalition, Template, 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 CSAR!")
    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="Red Cross"
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="IFRC"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="CSAR"
      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",             "*")           -- CSAR status update.
  self:AddTransition("*",             "PilotDown",          "*")          -- Downed Pilot added
  self:AddTransition("*",             "Approach",           "*")         -- CSAR heli closing in.
  self:AddTransition("*",             "Landed",             "*")         -- CSAR heli landed
  self:AddTransition("*",             "Boarded",            "*")          -- Pilot boarded.
  self:AddTransition("*",             "Returning",          "*")        -- CSAR able to return to base.
  self:AddTransition("*",             "Rescued",            "*")          -- Pilot at MASH.
  self:AddTransition("*",             "KIA",                "*")          -- Pilot killed in action.
  self:AddTransition("*",             "Load",                "*")           -- CSAR load  event.
  self:AddTransition("*",             "Save",                "*")           -- CSAR save  event.
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop FSM.

  -- tables, mainly for tracking actions
  self.addedTo = {}
  self.allheligroupset = {} -- GROUP_SET of all helis
  self.csarUnits = {} -- table of CSAR unit names
  self.FreeVHFFrequencies = {}
  self.heliVisibleMessage = {} -- tracks if the first message has been sent of the heli being visible
  self.heliCloseMessage = {} -- tracks heli close message  ie heli < 500m distance
  self.hoverStatus = {} -- tracks status of a helis hover above a downed pilot
  self.inTransitGroups = {} -- contain a table for each SAR with all units he has with the original names
  self.landedStatus = {}
  self.lastCrash = {}
  self.takenOff = {}
  self.smokeMarkers = {} -- tracks smoke markers for groups
  self.UsedVHFFrequencies = {}
  self.woundedGroups = {} -- contains the new group of units
  self.downedPilots = {} -- Replacement woundedGroups
  self.downedpilotcounter = 1

  -- settings, counters etc
  self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH
  self.rescuedpilots = 0 -- counter for saved pilots
  self.csarOncrash = false -- If set to true, will generate a csar when a plane crashes as well.
  self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined arms.
  self.enableForAI = false -- set to false to disable AI units from being rescued.
  self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue
  self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates.
  self.immortalcrew = true -- Set to true to make wounded crew immortal
  self.invisiblecrew = false -- Set to true to make wounded crew insvisible
  self.messageTime = 15 -- Time to show longer messages for in seconds
  self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS
  self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters.
  self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter
  self.loadtimemax = 135 -- seconds
  self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK!
  self.beaconRefresher = 29 -- seconds
  self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase
  self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
  self.max_units = 6 --max number of pilots that can be carried
  self.useprefix = true  -- Use the Prefixed defined below, Requires Unit have the Prefix defined below
  self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names!
  self.template = Template or "generic" -- template for downed pilot
  self.mashprefix = {"MASH"} -- prefixes used to find MASHes

  self.autosmoke = false -- automatically smoke location when heli is near
  self.autosmokedistance = 2000 -- distance for autosmoke
  -- added 0.1.4
  self.limitmaxdownedpilots = true
  self.maxdownedpilots = 25
  -- generate Frequencies
  self:_GenerateVHFrequencies()
  -- added 0.1.8
  self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters
  self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters
  self.pilotmustopendoors = false -- switch to true to enable check on open doors
  self.suppressmessages = false

  -- added 0.1.11r1
  self.rescuehoverheight = 20
  self.rescuehoverdistance = 10

  -- added 0.1.12
  self.countryblue= country.id.USA
  self.countryred = country.id.RUSSIA
  self.countryneutral = country.id.UN_PEACEKEEPERS

  -- added 0.1.3
  self.csarUsePara = false -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection

  -- added 0.1.4
  self.wetfeettemplate = nil
  self.usewetfeet = false

  -- added 1.0.15
  self.allowbronco = false  -- set to true to use the Bronco mod as a CSAR plane

  self.ADFRadioPwr = 500

  -- added 1.0.16
  self.PilotWeight = 80

  -- Own SET_GROUP if any
  self.UserSetGroup = nil

  -- WARNING - here\'ll be dragons
  -- for this to work you need to de-sanitize your mission environment in <DCS root>\Scripts\MissionScripting.lua
  -- needs SRS => 1.9.6 to work (works on the *server* side)
  self.useSRS = false -- Use FF\'s SRS integration
  self.SRSPath = "E:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" -- adjust your own path in your server(!)
  self.SRSchannel = 300 -- radio channel
  self.SRSModulation = radio.modulation.AM -- modulation
  self.SRSport = 5002 -- port
  self.SRSCulture = "en-GB"
  self.SRSVoice = MSRS.Voices.Google.Standard.en_GB_Standard_B
  self.SRSGPathToCredentials = nil
  self.SRSVolume = 1.0 -- volume 0.0 to 1.0
  self.SRSGender = "male" -- male or female
  self.CSARVoice = MSRS.Voices.Google.Standard.en_US_Standard_A
  self.CSARVoiceMS = MSRS.Voices.Microsoft.Hedda
  self.coordinate = nil -- Core.Point#COORDINATE

  local AliaS = string.gsub(self.alias," ","_")
  self.filename = string.format("CSAR_%s_Persist.csv",AliaS)

  -- load and save downed pilots
  self.enableLoadSave = false
  self.filepath = nil
  self.saveinterval = 600

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers.
  -- @function [parent=#CSAR] Start
  -- @param #CSAR self

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

  --- Triggers the FSM event "Stop". Stops the CSAR and all its event handlers.
  -- @param #CSAR self

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

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

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

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

  --- Triggers the FSM event "Save".
  -- @function [parent=#CSAR] Load
  -- @param #CSAR self

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

  --- On After "PilotDown" event. Downed Pilot detected.
  -- @function [parent=#CSAR] OnAfterPilotDown
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP Group Group object of the downed pilot.
  -- @param #number Frequency Beacon frequency in kHz.
  -- @param #string Leadername Name of the #UNIT of the downed pilot.
  -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype.
  -- @param #string Playername Player name if any given. Might be nil!

  --- On After "Aproach" event. Heli close to downed Pilot.
  -- @function [parent=#CSAR] OnAfterApproach
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Heliname Name of the helicopter group.
  -- @param #string Woundedgroupname Name of the downed pilot\'s group.

  --- On After "Landed" event. Heli landed at an airbase.
  -- @function [parent=#CSAR] OnAfterLanded
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string HeliName Name of the #UNIT which has landed.
  -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed.

  --- On After "Boarded" event. Downed pilot boarded heli.
  -- @function [parent=#CSAR] OnAfterBoarded
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Heliname Name of the helicopter group.
  -- @param #string Woundedgroupname Name of the downed pilot\'s group.
  -- @param #string Description Descriptive name of the group.

  --- On After "Returning" event. Heli can return home with downed pilot(s).
  -- @function [parent=#CSAR] OnAfterReturning
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Heliname Name of the helicopter group.
  -- @param #string Woundedgroupname Name of the downed pilot\'s group.

  --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB.
  -- @function [parent=#CSAR] OnAfterRescued
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter.
  -- @param #string HeliName Name of the helicopter group.
  -- @param #number PilotsSaved Number of the saved pilots on board when landing.

  --- On After "KIA" event. Pilot is dead.
  -- @function [parent=#CSAR] OnAfterKIA
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Pilotname Name of the pilot KIA.

  --- FSM Function OnAfterLoad.
  -- @function [parent=#CSAR] OnAfterLoad
  -- @param #CSAR 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 "CSAR_<alias>_Persist.csv".

  --- FSM Function OnAfterSave.
  -- @function [parent=#CSAR] OnAfterSave
  -- @param #CSAR 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 "CSAR_<alias>_Persist.csv".

  return self
end

------------------------
--- Helper Functions ---
------------------------

--- (Internal) Function to insert downed pilot tracker object.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP Group The #GROUP object
-- @param #string Groupname Name of the spawned group.
-- @param #number Side Coalition.
-- @param #string OriginalUnit Name of original Unit.
-- @param #string Description Descriptive text.
-- @param #string Typename Typename of unit.
-- @param #number Frequency Frequency of the NDB in Hz
-- @param #string Playername Name of Player (if applicable)
-- @param #boolean Wetfeet Ejected over water
-- @return #CSAR self.
function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet,BeaconName)
  self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername})

  -- create new entry
  local DownedPilot = {} -- #CSAR.DownedPilot
  DownedPilot.desc = Description or ""
  DownedPilot.frequency = Frequency or 0
  DownedPilot.index = self.downedpilotcounter
  DownedPilot.name = Groupname or Playername or ""
  DownedPilot.originalUnit = OriginalUnit or ""
  DownedPilot.player = Playername or ""
  DownedPilot.side = Side or 0
  DownedPilot.typename = Typename or ""
  DownedPilot.group = Group
  DownedPilot.timestamp = 0
  DownedPilot.alive = true
  DownedPilot.wetfeet = Wetfeet or false
  DownedPilot.BeaconName = BeaconName

  -- Add Pilot
  local PilotTable = self.downedPilots
  local counter = self.downedpilotcounter
  PilotTable[counter] = {}
  PilotTable[counter] = DownedPilot
  self:T({Table=PilotTable})
  self.downedPilots = PilotTable
  -- Increase counter
  self.downedpilotcounter = self.downedpilotcounter+1
  return self
end

--- (Internal) Count pilots on board.
-- @param #CSAR self
-- @param #string _heliName
-- @return #number count
function CSAR:_PilotsOnboard(_heliName)
  self:T(self.lid .. " _PilotsOnboard")
  local count = 0
  if self.inTransitGroups[_heliName] then
    for _, _group in pairs(self.inTransitGroups[_heliName]) do
      count = count + 1
    end
  end
  return count
end

--- (Internal) Function to check for dupe eject events.
-- @param #CSAR self
-- @param #string _unitname Name of unit.
-- @return #boolean Outcome
function CSAR:_DoubleEjection(_unitname)
  if self.lastCrash[_unitname] then
    local _time = self.lastCrash[_unitname]
    if timer.getTime() - _time < 10 then
      self:E(self.lid.."Caught double ejection!")
      return true
    end
  end
  self.lastCrash[_unitname] = timer.getTime()
  return false
end

--- (User) Add a PLAYERTASK - FSM events will check success
-- @param #CSAR self
-- @param Ops.PlayerTask#PLAYERTASK PlayerTask
-- @return #CSAR self
function CSAR: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) Spawn a downed pilot
-- @param #CSAR self
-- @param #number country Country for template.
-- @param Core.Point#COORDINATE point Coordinate to spawn at.
-- @param #number frequency Frequency of the pilot's beacon
-- @param #boolean wetfeet Spawn is over water
-- @return Wrapper.Group#GROUP group The #GROUP object.
-- @return #string alias The alias name.
function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet)
  self:T({country,point,frequency,tostring(wetfeet)})
  local freq = frequency or 1000
  local freq = freq / 1000 -- kHz
  for i=1,10 do
    math.random(i,10000)
  end
  if point:IsSurfaceTypeWater() or wetfeet then
    point.y = 0
  end
  local template = self.template
  if self.usewetfeet and wetfeet then
    template = self.wetfeettemplate
  end
  local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99))
  local coalition = self.coalition
  local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA?
  local _spawnedGroup = SPAWN
    :NewWithAlias(template,alias)
    :InitCoalition(coalition)
    :InitCountry(country)
    :InitDelayOff()
    :SpawnFromCoordinate(point)

  return _spawnedGroup, alias -- Wrapper.Group#GROUP object
end

--- (Internal) Add options to a downed pilot
-- @param #CSAR self
-- @param Wrapper.Group#GROUP group Group to use.
function CSAR:_AddSpecialOptions(group)
  self:T(self.lid.." _AddSpecialOptions")
  self:T({group})

  local immortalcrew = self.immortalcrew
  local invisiblecrew = self.invisiblecrew
  if immortalcrew then
    local _setImmortal = {
      id = 'SetImmortal',
      params = {
        value = true
      }
    }
    group:SetCommand(_setImmortal)
  end

  if invisiblecrew then
    local _setInvisible = {
      id = 'SetInvisible',
      params = {
        value = true
      }
    }
    group:SetCommand(_setInvisible)
  end

  group:OptionAlarmStateGreen()
  group:OptionROEHoldFire()
  return self
end

--- (Internal) Function to spawn a CSAR object into the scene.
-- @param #CSAR self
-- @param #number _coalition Coalition
-- @param DCS#country.id _country Country ID
-- @param Core.Point#COORDINATE _point Coordinate
-- @param #string _typeName Typename
-- @param #string _unitName Unitname
-- @param #string _playerName Playername
-- @param #number _freq Frequency
-- @param #boolean noMessage
-- @param #string _description Description
-- @param #boolean forcedesc Use the description only for the pilot track entry
-- @return Wrapper.Group#GROUP PilotInField Pilot GROUP object
-- @return #string AliasName Alias display name
function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description, forcedesc )
  self:T(self.lid .. " _AddCsar")
  self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description})

  local template = self.template
  local wetfeet = false

  local surface = _point:GetSurfaceType()
  if surface == land.SurfaceType.WATER then
    wetfeet = true
  end

  if not _freq then
    _freq = self:_GenerateADFFrequency()
    if not _freq then _freq = 333000 end --noob catch
  end

  local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq,wetfeet)

  local _typeName = _typeName or "Pilot"

  if not noMessage then
    if _freq ~= 0 then --shagrat different CASEVAC msg
      self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime)
    else
      self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime)
    end
  end

  local BeaconName

  if _playerName then
    BeaconName = _playerName..math.random(1,10000)
  elseif _unitName then
    BeaconName = _unitName..math.random(1,10000)
  else
    BeaconName = "Ghost-1-1"..math.random(1,10000)
  end

  if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0
    self:_AddBeaconToGroup(_spawnedGroup, _freq, BeaconName)
  end

  self:_AddSpecialOptions(_spawnedGroup)

  local _text = _description
  if not forcedesc then
    if _playerName ~= nil then
      if _freq ~= 0 then --shagrat
        _text = "Pilot " .. _playerName
      else
        _text = "TIC - " .. _playerName
      end
    elseif _unitName ~= nil then
      if _freq ~= 0 then --shagrat
        _text = "AI Pilot of " .. _unitName
      else
        _text = "TIC - " .. _unitName
      end
    end
  end
  self:T({_spawnedGroup, _alias})

  local _GroupName = _spawnedGroup:GetName() or _alias

  self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet,BeaconName)

  self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage, _playerName) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc.

  return _spawnedGroup, _alias
end

--- (Internal) Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene.
-- @param #CSAR self
-- @param #string _zone Name of the zone. Can also be passed as a (normal, round) ZONE object.
-- @param #number _coalition Coalition.
-- @param #string _description (optional) Description.
-- @param #boolean _randomPoint (optional) Random yes or no.
-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string unitname (optional) Name of the lost unit.
-- @param #string typename (optional) Type of plane.
-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names.
function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc)
  self:T(self.lid .. " _SpawnCsarAtZone")
  local freq = self:_GenerateADFFrequency()

  local _triggerZone = nil
  if type(_zone) == "string" then
    _triggerZone = ZONE:New(_zone) -- trigger to use as reference position
  elseif type(_zone) == "table" and _zone.ClassName then
    if string.find(_zone.ClassName, "ZONE",1) then
      _triggerZone = _zone -- is already a zone
    end
  end

  if _triggerZone == nil then
    self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10)
    return
  end

  local _description = _description or "PoW"
  local unitname = unitname or "Old Rusty"
  local typename = typename or "Phantom II"

  local pos = {}
  if _randomPoint then
    local _pos =  _triggerZone:GetRandomPointVec3()
    pos = COORDINATE:NewFromVec3(_pos)
  else
    pos  = _triggerZone:GetCoordinate()
  end

  local _country = 0
  if _coalition == coalition.side.BLUE then
    _country = self.countryblue
  elseif _coalition == coalition.side.RED then
    _country = self.countryred
  else
    _country = self.countryneutral
  end

  self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc)

  return self
end

--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene.
-- @param #CSAR self
-- @param #string Zone Name of the zone. Can also be passed as a (normal, round) ZONE object.
-- @param #number Coalition Coalition.
-- @param #string Description (optional) Description.
-- @param #boolean RandomPoint (optional) Random yes or no.
-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string Unitname (optional) Name of the lost unit.
-- @param #string Typename (optional) Type of plane.
-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names.
-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so:
--
--        -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition
--        my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" )
function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc)
  self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc)
  return self
end

--- (Internal) Function to add a CSAR object into the scene at a Point coordinate (VEC_2). For mission designers wanting to add e.g. casualties to the scene, that don't use beacons.
-- @param #CSAR self
-- @param Core.Point#COORDINATE _Point
-- @param #number _coalition Coalition.
-- @param #string _description (optional) Description.
-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string unitname (optional) Name of the lost unit.
-- @param #string typename (optional) Type of plane.
-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names.
function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitname, typename, forcedesc) --shagrat added internal Function _SpawnCASEVAC
  self:T(self.lid .. " _SpawnCASEVAC")

  local _description = _description or "CASEVAC"
  local unitname = unitname or "CASEVAC"
  local typename = typename or "Ground Commander"

  local pos = {}
  pos  = _Point

  local _country = 0
  if _coalition == coalition.side.BLUE then
    _country = self.countryblue
  elseif _coalition == coalition.side.RED then
    _country = self.countryred
  else
    _country = self.countryneutral
  end
  --shagrat set frequency to 0 as "flag" for no beacon
  self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, 0, _nomessage, _description, forcedesc)

  return self
end

--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene.
-- @param #CSAR self
-- @param Core.Point#COORDINATE Point
-- @param #number Coalition Coalition.
-- @param #string Description (optional) Description.
-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string Unitname (optional) Name of the lost unit.
-- @param #string Typename (optional) Type of plane.
-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names.
-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so:
--
--        -- Create casualty  "CASEVAC" at coordinate Core.Point#COORDINATE for the blue coalition.
--        my_csar:SpawnCASEVAC( coordinate, coalition.side.BLUE )
function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc)
  self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc)
  return self
end --shagrat end added CASEVAC

--- (Internal) Event handler.
-- @param #CSAR self
function CSAR:_EventHandler(EventData)
  self:T(self.lid .. " _EventHandler")
  self:T({Event = EventData.id})

  local _event = EventData -- Core.Event#EVENTDATA

  -- no Player
  if self.enableForAI == false and _event.IniPlayerName == nil then
    return self
  end

  -- no event
  if _event == nil or _event.initiator == nil then
    return self

      -- take off
  elseif _event.id == EVENTS.Takeoff then -- taken off
    self:T(self.lid .. " Event unit - Takeoff")

    local _coalition = _event.IniCoalition
    if _coalition ~= self.coalition then
      return self --ignore!
    end

    if _event.IniGroupName then
      self.takenOff[_event.IniUnitName] = true
    end

    return self

      -- player enter unit
  elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit
    self:T(self.lid .. " Event unit - Player Enter")

    local _coalition = _event.IniCoalition
    self:T("Coalition = "..UTILS.GetCoalitionName(_coalition))
    if _coalition ~= self.coalition then
      return self --ignore!
    end

    if _event.IniPlayerName then
      self.takenOff[_event.IniPlayerName] = nil
    end

    -- jumped into flying plane?
    self:T("Taken Off: "..tostring(_event.IniUnit:InAir(true)))

    if _event.IniUnit:InAir(true) then
      self.takenOff[_event.IniPlayerName] = true
    end

    local _unit = _event.IniUnit
    local _group = _event.IniGroup

    local function IsBronco(Group)
      local grp = Group -- Wrapper.Group#GROUP
      local typename = grp:GetTypeName()
      self:T(typename)
      if typename == "Bronco-OV-10A" then return true end
      return false
    end

    if _unit:IsHelicopter() or _group:IsHelicopter() or IsBronco(_group) then
      self:_AddMedevacMenuItem()
    end

    return self

  elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then
    -- Pilot dead

    self:T(self.lid .. " Event unit - Pilot Dead")

    local _unit = _event.IniUnit
    local _unitname = _event.IniUnitName
    local _group = _event.IniGroup

    if _unit == nil then
      return self -- error!
    end

    local _coalition = _event.IniCoalition
    if _coalition ~= self.coalition then
      return self --ignore!
    end

    -- Catch multiple events here?
    if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then
      if self:_DoubleEjection(_unitname) then
        return self
      end

    else
      self:T(self.lid .. " Pilot has not taken off, ignore")
    end

    return self

  elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then
    if _event.id == EVENTS.PilotDead and self.csarOncrash == false then
      return self
    end
    self:T(self.lid .. " Event unit - Pilot Ejected")

    local _unit = _event.IniUnit
    local _unitname = _event.IniUnitName
    local _group = _event.IniGroup

    self:T({_unit.UnitName, _unitname, _group.GroupName})

    if _unit == nil then
      self:T("Unit NIL!")
      return self -- error!
    end

    --local _coalition = _unit:GetCoalition() -- nil now for some reason
    local _coalition = _group:GetCoalition()
    if _coalition ~= self.coalition then
      self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition))
      return self --ignore!
    end


    self:T("Airborne: "..tostring(_group:IsAirborne()))
    self:T("Taken Off: "..tostring(self.takenOff[_event.IniUnitName]))

    if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then
      self:T(self.lid .. " Pilot has not taken off, ignore")
      -- return self -- give up, pilot hasnt taken off
    end

    if self:_DoubleEjection(_unitname) then
      self:T("Double Ejection!")
      return self
    end


    local initdcscoord = nil
    local initcoord = nil
    if _event.id == EVENTS.Ejection and _event.TgtDCSUnit then
      initdcscoord = _event.TgtDCSUnit:getPoint()
      initcoord = COORDINATE:NewFromVec3(initdcscoord)
      self:T({initdcscoord})
    elseif _event.IniDCSUnit then
      initdcscoord = _event.IniDCSUnit:getPoint()
      initcoord = COORDINATE:NewFromVec3(initdcscoord)
      self:T({initdcscoord})
    end

    -- Remove downed pilot if already exists to replace with new one.
    if _event.IniPlayerName then
      local PilotTable = self.downedPilots --#CSAR.DownedPilot
      local _foundPilot = nil
      for _,_pilot in pairs(PilotTable) do
        if _pilot.player == _event.IniPlayerName and _pilot.alive == true then
          _foundPilot = _pilot
          break
        end
      end
      if _foundPilot then
        self:T("Downed pilot already exists!")
        _foundPilot.group:Destroy(false)
        self:_RemoveNameFromDownedPilots(_foundPilot.name)
        self:_CheckDownedPilotTable()
      end
    end

    -- limit no of pilots in the field.
    if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then
      self:T("Maxed Downed Pilot!")
      return self
    end


    -- TODO: Over water check --- EVENTS.LandingAfterEjection NOT triggered by DCS, so handle csarUsePara = true case
    -- might create dual pilots in edge cases

    local wetfeet = false

    --local surface = _unit:GetCoordinate():GetSurfaceType()
    local surface = initcoord:GetSurfaceType()

    if surface == land.SurfaceType.WATER then
      self:T("Wet feet!")
      wetfeet = true
    end
    -- all checks passed, get going.
    if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land
      local _freq = self:_GenerateADFFrequency()
       self:_AddCsar(_coalition, _unit:GetCountry(), initcoord , _unit:GetTypeName(),  _unit:GetName(), _event.IniPlayerName, _freq, self.suppressmessages, "none")
      return self
    end

  elseif _event.id == EVENTS.Land then
    self:T(self.lid .. " Landing")

    if _event.IniUnitName then
      self.takenOff[_event.IniUnitName] = nil
    end

    if self.allowFARPRescue then

      local _unit = _event.IniUnit  -- Wrapper.Unit#UNIT

      if _unit == nil then
        self:T(self.lid .. " Unit nil on landing")
        return self -- error!
      end

      --local _coalition = _event.IniCoalition
      local _coalition = _event.IniGroup:GetCoalition()
      if _coalition ~= self.coalition then
        self:T(self.lid .. " Wrong coalition")
        return self --ignore!
      end

      self.takenOff[_event.IniUnitName] = nil

      local _place = _event.Place -- Wrapper.Airbase#AIRBASE

      if _place == nil then
        self:T(self.lid .. " Landing Place Nil")
        return self -- error!
      end

      -- anyone on board?
      if self.inTransitGroups[_event.IniUnitName] == nil then
        -- ignore
        return self
      end

      if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then
        self:__Landed(2,_event.IniUnitName, _place)
        local IsHeloBase = false
        local ABName = _place:GetName()
        if ABName and string.find(ABName,"^H") then IsHeloBase = true end -- if name starts with an H it's an (possibly elevated) helo base on current maps
        self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true,true,IsHeloBase)
      else
        self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition()))
      end
    end

    return self
  end

  ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location
  if (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then
    self:T("LANDING_AFTER_EJECTION")
    local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p)
    local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute'
    local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot"
    local _country = _event.initiator:getCountry()
    local _coalition = coalition.getCountryCoalition( _country )
    self:T("Country = ".._country.." Coalition = ".._coalition)
    if _coalition == self.coalition then
      local _freq = self:_GenerateADFFrequency()
      self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq})
      self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, self.suppressmessages, "none")--shagrat add CSAR at Parachute location.
    
      Unit.destroy(_event.initiator) -- shagrat remove static Pilot model
    end
  end

  return self
end

--- (Internal)  Initialize the action for a pilot.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP _downedGroup The group to rescue.
-- @param #string _GroupName Name of the Group
-- @param #number _freq Beacon frequency.
-- @param #boolean _nomessage Send message true or false.
-- @param #string _playername Name of the downed pilot if any
function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage, _playername)
  self:T(self.lid .. " _InitSARForPilot")
  local _leader = _downedGroup:GetUnit(1)
  local _groupName = _GroupName
  local _freqk = _freq / 1000
  local _coordinatesText = self:_GetPositionOfWounded(_downedGroup)
  local _leadername = _leader:GetName()

  if not _nomessage then
    if _freq ~= 0 then --shagrat
      local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute'
      if self.coordtype ~= 2 then --not MGRS
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
      else
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true)
        local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true)
        local _text = string.format("%s requests SAR at %s, beacon at %.2f kilo hertz", _groupName, coordtext, _freqk)
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false)
      end
    else --shagrat CASEVAC msg
      local _text = string.format("Pickup Zone at %s.", _coordinatesText )
      if self.coordtype ~= 2 then --not MGRS
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
      else
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true)
        local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true)
        local _text = string.format("Pickup Zone at %s.", coordtext )
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false)
      end
    end
  end

  for _,_heliName in pairs(self.csarUnits) do
    self:_CheckWoundedGroupStatus(_heliName, _groupName)
  end

  -- trigger FSM event
  self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText, _playername)

  return self
end

--- (Internal) Check if a name is in downed pilot table
-- @param #CSAR self
-- @param #string name Name to search for.
-- @return #boolean Outcome.
-- @return #CSAR.DownedPilot Table if found else nil.
function CSAR:_CheckNameInDownedPilots(name)
  local PilotTable = self.downedPilots --#CSAR.DownedPilot
  local found = false
  local table = nil
  for _,_pilot in pairs(PilotTable) do
    if _pilot.name == name and _pilot.alive == true then
      found = true
      table = _pilot
      break
    end
  end
  return found, table
end

--- (Internal) Check if a name is in downed pilot table and remove it.
-- @param #CSAR self
-- @param #string name Name to search for.
-- @param #boolean force Force removal.
-- @return #boolean Outcome.
function CSAR:_RemoveNameFromDownedPilots(name,force)
  local PilotTable = self.downedPilots --#CSAR.DownedPilot
  local found = false
  for _index,_pilot in pairs(PilotTable) do
    if _pilot.name == name then
      self.downedPilots[_index].alive = false
    end
  end
  return found
end

--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns.
-- @param #CSAR self
-- @param #boolean ShortCallsign If true, only call out the major flight number
-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers.
-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized
-- callsigns from playername or group name.
-- @return #CSAR self
function CSAR:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations)
  if not ShortCallsign or ShortCallsign == false then
    self.ShortCallsign = false
  else
    self.ShortCallsign = true
  end
  self.Keepnumber = Keepnumber or false
  self.CallsignTranslations = CallsignTranslations
  return self
end

--- (Internal) Check if a name is in downed pilot table and remove it.
-- @param #CSAR self
-- @param #string UnitName
-- @return #string CallSign
function CSAR:_GetCustomCallSign(UnitName)
  local callsign = UnitName
  local unit = UNIT:FindByName(UnitName)
  if unit and unit:IsAlive() then
    local group = unit:GetGroup()
    callsign = group:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations)
  end
  return callsign
end

--- (Internal) Check state of wounded group.
-- @param #CSAR self
-- @param #string heliname heliname
-- @param #string woundedgroupname woundedgroupname
function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname)
  self:T(self.lid .. " _CheckWoundedGroupStatus")
  local _heliName = heliname
  local _woundedGroupName = woundedgroupname
  self:T({Heli = _heliName, Downed  = _woundedGroupName})
  -- if wounded group is not here then message already been sent to SARs
  -- stop processing any further
  local _found, _downedpilot = self:_CheckNameInDownedPilots(_woundedGroupName)
  if not _found then
    self:T("...not found in list!")
    return
  end

  local _woundedGroup = _downedpilot.group
  if _woundedGroup ~= nil and _woundedGroup:IsAlive() then
    local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT

    local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking

    if _heliUnit == nil then
      self.heliVisibleMessage[_lookupKeyHeli] = nil
      self.heliCloseMessage[_lookupKeyHeli] = nil
      self.landedStatus[_lookupKeyHeli] = nil
      self:T("...heliunit nil!")
      return
    end

    local _heliCoord = _heliUnit:GetCoordinate()
    local _leaderCoord = _woundedGroup:GetCoordinate()
    local _distance = self:_GetDistance(_heliCoord,_leaderCoord)
    -- autosmoke
    if (self.autosmoke == true) and (_distance < self.autosmokedistance) and (_distance ~= -1) then
      self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup)
    end

    if _distance < self.approachdist_near and _distance > 0 then
      if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then
        -- we\'re close, reschedule
        _downedpilot.timestamp = timer.getAbsTime()
        self:__Approach(-5,heliname,woundedgroupname)
      end
    elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then
      -- message once
      if self.heliVisibleMessage[_lookupKeyHeli] == nil then
        local _pilotName = _downedpilot.desc
        if self.autosmoke == true then
          local dist = self.autosmokedistance / 1000
          local disttext = string.format("%.0fkm",dist)
          if _SETTINGS:IsImperial() then
            local dist = UTILS.MetersToNM(self.autosmokedistance)
            disttext = string.format("%.0fnm",dist)
          end
          self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", self:_GetCustomCallSign(_heliName), _pilotName, disttext), self.messageTime,false,true)
        else
          self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true)
        end
        --mark as shown for THIS heli and THIS group
        self.heliVisibleMessage[_lookupKeyHeli] = true
      end
      self.heliCloseMessage[_lookupKeyHeli] = nil
      self.landedStatus[_lookupKeyHeli] = nil
      --reschedule as units aren\'t dead yet , schedule for a bit slower though as we\'re far away
      _downedpilot.timestamp = timer.getAbsTime()
      self:__Approach(-10,heliname,woundedgroupname)
    end
  else
    self:T("...Downed Pilot KIA?!")
    if not _downedpilot.alive then
      --self:__KIA(1,_downedpilot.name)
      self:_RemoveNameFromDownedPilots(_downedpilot.name, true)
    end
  end
  return self
end

--- (Internal) Function to pop a smoke at a wounded pilot\'s positions.
-- @param #CSAR self
-- @param #string _woundedGroupName Name of the group.
-- @param Wrapper.Group#GROUP _woundedLeader Object of the group.
function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader)
  self:T(self.lid .. " _PopSmokeForGroup")
  -- have we popped smoke already in the last 5 mins
  local _lastSmoke = self.smokeMarkers[_woundedGroupName]
  if _lastSmoke == nil or timer.getTime() > _lastSmoke then

    local _smokecolor = self.smokecolor
    local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot
    _smokecoord:Smoke(_smokecolor)
    self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time
  end
  return self
end

--- (Internal) Function to pickup the wounded pilot from the ground.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heliUnit Object of the group.
-- @param #string _pilotName Name of the pilot.
-- @param Wrapper.Group#GROUP _woundedGroup Object of the group.
-- @param #string _woundedGroupName Name of the group.
function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
  self:T(self.lid .. " _PickupUnit")
  -- board
  local _heliName = _heliUnit:GetName()
  local _groups = self.inTransitGroups[_heliName]
  local _unitsInHelicopter = self:_PilotsOnboard(_heliName)

  -- init table if there is none for this helicopter
  if not _groups then
    self.inTransitGroups[_heliName] = {}
    _groups = self.inTransitGroups[_heliName]
  end

  -- if the heli can\'t pick them up, show a message and return
  local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()]
  if _maxUnits == nil then
    _maxUnits = self.max_units
  end
  if _unitsInHelicopter + 1 > _maxUnits then
    self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, self:_GetCustomCallSign(_heliName), _unitsInHelicopter, _unitsInHelicopter), self.messageTime,false,false,true)
    return self
  end

  local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName)
  local grouptable = downedgrouptable --#CSAR.DownedPilot
  self.inTransitGroups[_heliName][_woundedGroupName] =
    {
      originalUnit = grouptable.originalUnit,
      woundedGroup = _woundedGroupName,
      side = self.coalition,
      desc = grouptable.desc,
      player = grouptable.player,
    }

  _woundedGroup:Destroy(false)
  self:_RemoveNameFromDownedPilots(_woundedGroupName,true)

  self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,true,true)

  self:_UpdateUnitCargoMass(_heliName)

  self:__Boarded(5,_heliName,_woundedGroupName,grouptable.desc)

  return self
end

--- (Internal) Function to calculate and set Unit internal cargo mass
-- @param #CSAR self
-- @param #string _heliName Unit name
-- @return #CSAR self
function CSAR:_UpdateUnitCargoMass(_heliName)
  self:T(self.lid .. " _UpdateUnitCargoMass")
  local calculatedMass = self:_PilotsOnboard(_heliName)*(self.PilotWeight or 80)
  local Unit = UNIT:FindByName(_heliName)
  if Unit then
    Unit:SetUnitInternalCargo(calculatedMass)
  end
  return self
end

--- (Internal) Move group to destination.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP _leader
-- @param Core.Point#COORDINATE _destination
function CSAR:_OrderGroupToMoveToPoint(_leader, _destination)
  self:T(self.lid .. " _OrderGroupToMoveToPoint")
  local group = _leader
  local coordinate = _destination:GetVec2()

  group:SetAIOn()
  group:RouteToVec2(coordinate,5)
  return self
end

--- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze.
-- @param #CSAR self
-- @param #string unit_name Name of unit.
-- @return #boolean outcome The outcome.
function CSAR:_IsLoadingDoorOpen( unit_name )
  self:T(self.lid .. " _IsLoadingDoorOpen")
  return UTILS.IsLoadingDoorOpen(unit_name)
end

--- (Internal) Function to check if heli is close to group.
-- @param #CSAR self
-- @param #number _distance
-- @param Wrapper.Unit#UNIT _heliUnit
-- @param #string _heliName
-- @param Wrapper.Group#GROUP _woundedGroup
-- @param #string _woundedGroupName
-- @return #boolean Outcome
function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName)
  self:T(self.lid .. " _CheckCloseWoundedGroup")

  local _woundedLeader = _woundedGroup
  local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking

  local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot
  local _pilotName = _pilotable.desc


  local _reset = true

  if (_distance < 500) then
    self:T(self.lid .. "[Pickup Debug] Helo closer than 500m: ".._lookupKeyHeli)
    if self.heliCloseMessage[_lookupKeyHeli] == nil then
      if self.autosmoke == true then
        self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true)
      else
        self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true)
      end
      self.heliCloseMessage[_lookupKeyHeli] = true
    end
    self:T(self.lid .. "[Pickup Debug] Checking landed vs Hover for ".._lookupKeyHeli)
    -- have we landed close enough?
    if not _heliUnit:InAir() then
      self:T(self.lid .. "[Pickup Debug] Helo landed: ".._lookupKeyHeli)
      if self.pilotRuntoExtractPoint == true then
        if (_distance < self.extractDistance) then
          local _time = self.landedStatus[_lookupKeyHeli]
          self:T(self.lid .. "[Pickup Debug] Check pilot running or arrived ".._lookupKeyHeli)
          if _time == nil then
            self:T(self.lid .. "[Pickup Debug] Pilot running not arrived yet ".._lookupKeyHeli)
            self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 )
            _time = self.landedStatus[_lookupKeyHeli]
            _woundedGroup:OptionAlarmStateGreen()
            self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate())
            self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false)
          else
            _time = self.landedStatus[_lookupKeyHeli] - 10
            self.landedStatus[_lookupKeyHeli] = _time
          end
          --if _time <= 0 or _distance < self.loadDistance then
          self:T(self.lid .. "[Pickup Debug] Pilot close enough? ".._lookupKeyHeli)
          if _distance < self.loadDistance + 5 or _distance <= 13 then
            self:T(self.lid .. "[Pickup Debug] Pilot close enough - YES ".._lookupKeyHeli)
            if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
              self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
              self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli)
              return false
            else
              self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli)
              self.landedStatus[_lookupKeyHeli] = nil
              self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
              return true
            end
          end
        end
      else
        self:T(self.lid .. "[Pickup Debug] Helo landed, pilot NOT set to run to helo ".._lookupKeyHeli)
        if (_distance < self.loadDistance) then
          self:T(self.lid .. "[Pickup Debug] Helo close enough, door check ".._lookupKeyHeli)
          if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
            self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli)
            self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
            return false
          else
            self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli)
            self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
            return true
          end
        end
      end
    else
      self:T(self.lid .. "[Pickup Debug] Helo hovering".._lookupKeyHeli)
      local _unitsInHelicopter = self:_PilotsOnboard(_heliName)
      local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()]
      if _maxUnits == nil then
        _maxUnits = self.max_units
      end
      self:T(self.lid .. "[Pickup Debug] Check capacity and close enough for winching ".._lookupKeyHeli)
      if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then
        -- DONE - make variable
        if _distance < self.rescuehoverdistance then
          self:T(self.lid .. "[Pickup Debug] Helo hovering close enough ".._lookupKeyHeli)
          --check height!
          local leaderheight = _woundedLeader:GetHeight()
          if leaderheight < 0 then leaderheight = 0 end
          local _height = _heliUnit:GetHeight() - leaderheight

          -- DONE - make variable
          if _height <= self.rescuehoverheight then
            self:T(self.lid .. "[Pickup Debug] Helo hovering low enough ".._lookupKeyHeli)
            local _time = self.hoverStatus[_lookupKeyHeli]

            if _time == nil then
              self.hoverStatus[_lookupKeyHeli] = 10
              _time = 10
            else
              _time = self.hoverStatus[_lookupKeyHeli] - 10
              self.hoverStatus[_lookupKeyHeli] = _time
            end
            self:T(self.lid .. "[Pickup Debug] Check hover timer ".._lookupKeyHeli)
            if _time > 0 then
              self:T(self.lid .. "[Pickup Debug] Helo hovering not long enough ".._lookupKeyHeli)
              self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true)
            else
              self:T(self.lid .. "[Pickup Debug] Helo hovering long enough - door check ".._lookupKeyHeli)
              if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
                self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
                self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli)
                return false
              else
                self.hoverStatus[_lookupKeyHeli] = nil
                self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
                self:T(self.lid .. "[Pickup Debug] Pilot picked up ".._lookupKeyHeli)
                return true
              end
            end
            _reset = false
          else
            self:T(self.lid .. "[Pickup Debug] Helo hovering too high ".._lookupKeyHeli)
            self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true)
            self:T(self.lid .. "[Pickup Debug] Hovering too high, try again next loop ".._lookupKeyHeli)
            return false
          end
        end

      end
    end
  end

  if _reset then
    self.hoverStatus[_lookupKeyHeli] = nil
  end

  if _distance < 500 then
    return true
  else
    return false
  end
end

--- (Internal) Monitor in-flight returning groups.
-- @param #CSAR self
-- @param #string heliname Heli name
-- @param #string groupname Group name
-- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP
-- @param #boolean noreschedule If true, do not try to reschedule this if distances are not ok (coming from landing event)
-- @param #boolean IsHeloBase If true, landing took place at a Helo Base  (name "H ..." on current maps)
function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule, IsHeloBase)
  self:T(self.lid .. " _ScheduledSARFlight")
  self:T({heliname,groupname})
  local _heliUnit = self:_GetSARHeli(heliname)
  local _woundedGroupName = groupname

  if (_heliUnit == nil) then
    --helicopter crashed?
    self.inTransitGroups[heliname] = nil
    return
  end

  if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then
    -- Groups already rescued
    return
  end

  local _dist = self:_GetClosestMASH(_heliUnit)

  if _dist == -1 then
    self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance can not be determined!")
    return
  end

  self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance km: "..math.floor(_dist/1000))

  if ( _dist < self.FARPRescueDistance or isairport ) and ((_heliUnit:InAir() == false) or (IsHeloBase == true)) then
    self:T(self.lid.."[Drop off debug] Distance ok, door check")
    if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then
      self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true, true)
      self:T(self.lid.."[Drop off debug] Door closed, try again next loop")
    else
      self:T(self.lid.."[Drop off debug] Rescued!")
      self:_RescuePilots(_heliUnit)
      return
    end
  end

  --queue up
  if not noreschedule then
    self:__Returning(5,heliname,_woundedGroupName, isairport)
    self:ScheduleOnce(5,self._ScheduledSARFlight,self,heliname,groupname, isairport, noreschedule, IsHeloBase)
  end
  return self
end

--- (Internal) Mark pilot as rescued and remove from tables.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heliUnit
function CSAR:_RescuePilots(_heliUnit)
  self:T(self.lid .. " _RescuePilots")
  local _heliName = _heliUnit:GetName()
  local _rescuedGroups = self.inTransitGroups[_heliName]

  if _rescuedGroups == nil then
    -- Groups already rescued
    return
  end

  local PilotsSaved = self:_PilotsOnboard(_heliName)

  self.inTransitGroups[_heliName] = nil

  local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", self:_GetCustomCallSign(_heliName), PilotsSaved)

  self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime)

  self:_UpdateUnitCargoMass(_heliName)

  -- trigger event
  self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved)
  return self
end

--- (Internal) Check and return Wrappe.Unit#UNIT based on the name if alive.
-- @param #CSAR self
-- @param #string _unitname Name of Unit
-- @return Wrapper.Unit#UNIT The unit or nil
function CSAR:_GetSARHeli(_unitName)
  self:T(self.lid .. " _GetSARHeli")
  local unit = UNIT:FindByName(_unitName)
  if unit and unit:IsAlive() then
    return unit
  else
    return nil
  end
end

--- (Internal) Display message to single Unit.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _unit Unit #UNIT to display to.
-- @param #string _text Text of message.
-- @param #number _time Message show duration.
-- @param #boolean _clear (optional) Clear screen.
-- @param #boolean _speak (optional) Speak message via SRS.
-- @param #boolean _override (optional) Override message suppression
function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak, _override)
  self:T(self.lid .. " _DisplayMessageToSAR")
  local group = _unit:GetGroup()
  local _clear = _clear or nil
  local _time = _time or self.messageTime
  if _override or not self.suppressmessages then
    local m = MESSAGE:New(_text,_time,"CSAR",_clear):ToGroup(group)
  end
  -- integrate SRS
  if _speak and self.useSRS then
    local coord = _unit:GetCoordinate()
    if coord then
      self.msrs:SetCoordinate(coord)
    end
    _text = string.gsub(_text,"km"," kilometer")
    _text = string.gsub(_text,"nm"," nautical miles")
    self.SRSQueue:NewTransmission(_text,duration,self.msrs,tstart,2,subgroups,subtitle,subduration,self.SRSchannel,self.SRSModulation,gender,culture,self.SRSVoice,volume,label,coord)
  end
  return self
end

--- (Internal) Function to get string of a group\'s position.
-- @param #CSAR self
-- @param Wrapper.Controllable#CONTROLLABLE _woundedGroup Group or Unit object.
-- @param Wrapper.Unit#UNIT _Unit Requesting helo pilot unit
-- @return #string Coordinates as Text
function CSAR:_GetPositionOfWounded(_woundedGroup,_Unit)
  self:T(self.lid .. " _GetPositionOfWounded")
  local _coordinate = _woundedGroup:GetCoordinate()
  local _coordinatesText = "None"
  if _coordinate then
    if self.coordtype == 0 then -- Lat/Long DMTM
      _coordinatesText = _coordinate:ToStringLLDDM()
    elseif self.coordtype == 1 then -- Lat/Long DMS
      _coordinatesText = _coordinate:ToStringLLDMS()
    elseif self.coordtype == 2 then -- MGRS
      _coordinatesText = _coordinate:ToStringMGRS()
    else -- Bullseye Metric --(medevac.coordtype == 4 or 3)
      _coordinatesText = _coordinate:ToStringBULLS(self.coalition)
    end
  end
  if _Unit and _Unit:GetPlayerName() then
    local playername = _Unit:GetPlayerName()
    if playername then
      local settings = _DATABASE:GetPlayerSettings(playername) or _SETTINGS
      if settings then
        self:T("Get Settings ok!")
        if settings:IsA2G_MGRS() then
          _coordinatesText = _coordinate:ToStringMGRS(settings)
        elseif settings:IsA2G_LL_DMS() then
          _coordinatesText = _coordinate:ToStringLLDMS(settings)
        elseif settings:IsA2G_LL_DDM() then
          _coordinatesText = _coordinate:ToStringLLDDM(settings)
        elseif settings:IsA2G_BR() then
          -- attention this is the distance from the ASKING unit to target, not from RECCE to target!
          local startcoordinate = _Unit:GetCoordinate()
          _coordinatesText = _coordinate:ToStringBR(startcoordinate,settings)
        end
      end
    end
  end
  return _coordinatesText
end

--- (Internal) Display active SAR tasks to player.
-- @param #CSAR self
-- @param #string _unitName Unit to display to
function CSAR:_DisplayActiveSAR(_unitName)
  self:T(self.lid .. " _DisplayActiveSAR")
  local _msg = "Active MEDEVAC/SAR:"
  local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT
  if _heli == nil then
    return
  end

  local _heliSide = self.coalition
  local _csarList = {}

  local _DownedPilotTable = self.downedPilots
  self:T({Table=_DownedPilotTable})
  for _, _value in pairs(_DownedPilotTable) do
    local _groupName = _value.name
    self:T(string.format("Display Active Pilot: %s", tostring(_groupName)))
    self:T({Table=_value})
    local _woundedGroup = _value.group
    if _woundedGroup and _value.alive then
      local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup,_heli)
      local _helicoord =  _heli:GetCoordinate()
      local _woundcoord = _woundedGroup:GetCoordinate()
      local _distance = self:_GetDistance(_helicoord, _woundcoord)
      self:T({_distance = _distance})
      local distancetext = ""
      local settings = _SETTINGS
      if _heli:GetPlayerName() then
        settings = _DATABASE:GetPlayerSettings(_heli:GetPlayerName()) or _SETTINGS
      end
      if settings:IsImperial() then
        distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance))
      else
        distancetext = string.format("%.1fkm", _distance/1000.0)
      end
      if _value.frequency == 0 or self.CreateRadioBeacons == false then--shagrat insert CASEVAC without Frequency
        table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) })
      else
        table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) })
      end
    end
  end

  local function sortDistance(a, b)
    return a.dist < b.dist
  end

  table.sort(_csarList, sortDistance)

  for _, _line in pairs(_csarList) do
    _msg = _msg .. "\n" .. _line.msg
  end

  self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2, false, false, true)
  return self
end

--- (Internal) Find the closest downed pilot to a heli.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT
-- @return #table Table of results
function CSAR:_GetClosestDownedPilot(_heli)
  self:T(self.lid .. " _GetClosestDownedPilot")
  local _side = self.coalition
  local _closestGroup = nil
  local _shortestDistance = -1
  local _distance = 0
  local _closestGroupInfo = nil
  local _heliCoord = _heli:GetCoordinate() or _heli:GetCoordinate()

  if _heliCoord == nil then
    self:E("****Error obtaining coordinate!")
    return nil
  end

  local DownedPilotsTable = self.downedPilots

  for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do
    --for _, _groupInfo in pairs(DownedPilotsTable) do
    local _woundedName = _groupInfo.name
    local _tempWounded = _groupInfo.group

    -- check group exists and not moving to someone else
    if _tempWounded then
      local _tempCoord = _tempWounded:GetCoordinate()
      _distance = self:_GetDistance(_heliCoord, _tempCoord)

      if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
        _shortestDistance = _distance
        _closestGroup = _tempWounded
        _closestGroupInfo = _groupInfo
      end
    end
  end

  return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo }
end

--- (Internal) Fire a flare at the point of a downed pilot.
-- @param #CSAR self
-- @param #string _unitName Name of the unit.
function CSAR:_SignalFlare(_unitName)
  self:T(self.lid .. " _SignalFlare")
  local _heli = self:_GetSARHeli(_unitName)
  if _heli == nil then
    return
  end

  local _closest = self:_GetClosestDownedPilot(_heli)
  local smokedist = 8000
  if self.approachdist_far > smokedist then smokedist = self.approachdist_far end
  if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then

    local _clockDir = self:_GetClockDirection(_heli, _closest.pilot)
    local _distance = ""
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance))
    else
      _distance = string.format("%.1fkm",_closest.distance/1000)
    end
    local _msg = string.format("%s - Firing signal flare at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance)
    self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)

    local _coord = _closest.pilot:GetCoordinate()
    _coord:FlareRed(_clockDir)
  else
    local _distance = smokedist
    local dtext = ""
    if _SETTINGS:IsImperial() then
      dtext = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
    else
      dtext = string.format("%.1fkm",smokedist/1000)
    end
    self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",dtext), self.messageTime, false, false, true)
  end
  return self
end

--- (Internal) Display info to all SAR groups.
-- @param #CSAR self
-- @param #string _message Message to display.
-- @param #number _side Coalition of message.
-- @param #number _messagetime How long to show.
-- @param #boolean ToSRS If true or nil, send to SRS TTS
-- @param #boolean ToScreen If true or nil, send to Screen
function CSAR:_DisplayToAllSAR(_message, _side, _messagetime,ToSRS,ToScreen)
  self:T(self.lid .. " _DisplayToAllSAR")
  local messagetime = _messagetime or self.messageTime
  self:T({_message,ToSRS=ToSRS,ToScreen=ToScreen})
  if self.msrs and (ToSRS == true or ToSRS == nil) then
    local voice = self.CSARVoice or MSRS.Voices.Google.Standard.en_GB_Standard_F
    if self.msrs:GetProvider() == MSRS.Provider.WINDOWS then
      voice = self.CSARVoiceMS or MSRS.Voices.Microsoft.Hedda
    end
    --self:F("Voice = "..voice)
    self.SRSQueue:NewTransmission(_message,duration,self.msrs,tstart,2,subgroups,subtitle,subduration,self.SRSchannel,self.SRSModulation,gender,culture,voice,volume,label,self.coordinate)
  end
  if ToScreen == true or ToScreen == nil then
    for _, _unitName in pairs(self.csarUnits) do
      local _unit = self:_GetSARHeli(_unitName)
      if _unit and not self.suppressmessages then
        self:_DisplayMessageToSAR(_unit, _message, _messagetime)
      end
    end
  end
  return self
end

---(Internal) Request IR Strobe at closest downed pilot.
--@param #CSAR self
--@param #string _unitName Name of the helicopter
function CSAR:_ReqIRStrobe( _unitName )
  self:T(self.lid .. " _ReqIRStrobe")
  local _heli = self:_GetSARHeli(_unitName)
  if _heli == nil then
    return
  end
  local smokedist = 8000
  if smokedist < self.approachdist_far then smokedist = self.approachdist_far end
  local _closest = self:_GetClosestDownedPilot(_heli)
  if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then
    local _clockDir = self:_GetClockDirection(_heli, _closest.pilot)
    local _distance = string.format("%.1fkm",_closest.distance/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance))
    else
      _distance = string.format("%.1fkm",_closest.distance/1000)
    end
    local _msg = string.format("%s - IR Strobe active at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance)
    self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)
    _closest.pilot:NewIRMarker(true,self.IRStrobeRuntime or 300)
  else
    local _distance = string.format("%.1fkm",smokedist/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
    else
      _distance = string.format("%.1fkm",smokedist/1000)
    end
    self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true)
  end
  return self
end

---(Internal) Request smoke at closest downed pilot.
--@param #CSAR self
--@param #string _unitName Name of the helicopter
function CSAR:_Reqsmoke( _unitName )
  self:T(self.lid .. " _Reqsmoke")
  local _heli = self:_GetSARHeli(_unitName)
  if _heli == nil then
    return
  end
  local smokedist = 8000
  if smokedist < self.approachdist_far then smokedist = self.approachdist_far end
  local _closest = self:_GetClosestDownedPilot(_heli)
  if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then
    local _clockDir = self:_GetClockDirection(_heli, _closest.pilot)
    local _distance = string.format("%.1fkm",_closest.distance/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance))
    else
      _distance = string.format("%.1fkm",_closest.distance/1000)
    end
    local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance)
    self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)
    local _coord = _closest.pilot:GetCoordinate()
    local color = self.smokecolor
    _coord:Smoke(color)
  else
    local _distance = string.format("%.1fkm",smokedist/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
    else
      _distance = string.format("%.1fkm",smokedist/1000)
    end
    self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true)
  end
  return self
end

--- (Internal) Determine distance to closest MASH.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT
-- @return #number Distance in meters
-- @return #string MASH Name as string
function CSAR:_GetClosestMASH(_heli)
  self:T(self.lid .. " _GetClosestMASH")
  local _mashset = self.mash -- Core.Set#SET_GROUP
  local MashSets = {}
  --local _mashes = _mashset.Set-- #table
  table.insert(MashSets,_mashset.Set)
  table.insert(MashSets,self.zonemashes.Set)
  table.insert(MashSets,self.staticmashes.Set)
  local _shortestDistance = -1
  local _distance = 0
  local _helicoord = _heli:GetCoordinate()
  local MashName = nil

  if self.allowFARPRescue then
    local position = _heli:GetCoordinate()
    local afb,distance = position:GetClosestAirbase(nil,self.coalition)
    _shortestDistance = distance
    MashName = (afb ~= nil) and afb:GetName() or "Unknown"
  end

  for _,_mashes in pairs(MashSets)  do
    for _, _mashUnit in pairs(_mashes or {}) do
      local _mashcoord
      if _mashUnit and (not _mashUnit:IsInstanceOf("ZONE_BASE")) and _mashUnit:IsAlive() then
        _mashcoord = _mashUnit:GetCoordinate()
      elseif _mashUnit and _mashUnit:IsInstanceOf("ZONE_BASE") then
        _mashcoord = _mashUnit:GetCoordinate()
      end
      _distance = self:_GetDistance(_helicoord, _mashcoord)
      if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
        _shortestDistance = _distance
        MashName = _mashUnit:GetName() or "Unknown"
      end
    end
  end

  if _shortestDistance ~= -1 then
    return _shortestDistance, MashName
  else
    return -1
  end

end

--- (Internal) Display onboarded rescued pilots.
-- @param #CSAR self
-- @param #string _unitName Name of the chopper
function CSAR:_CheckOnboard(_unitName)
  self:T(self.lid .. " _CheckOnboard")
  local _unit = self:_GetSARHeli(_unitName)
  if _unit == nil then
    return
  end
  --list onboard pilots
  local _inTransit = self.inTransitGroups[_unitName]
  if _inTransit == nil then
    self:_DisplayMessageToSAR(_unit, "No Rescued Pilots onboard", self.messageTime, false, false, true)
  else
    local _text = "Onboard - RTB to FARP/Airfield or MASH: "
    for _, _onboard in pairs(self.inTransitGroups[_unitName]) do
      _text = _text .. "\n" .. _onboard.desc
    end
    self:_DisplayMessageToSAR(_unit, _text, self.messageTime*2, false, false, true)
  end
  return self
end

--- (Internal) Populate F10 menu for CSAR players.
-- @param #CSAR self
function CSAR:_AddMedevacMenuItem()
  self:T(self.lid .. " _AddMedevacMenuItem")

  local coalition = self.coalition
  local allheligroupset = self.allheligroupset -- Core.Set#SET_GROUP
  local _allHeliGroups = allheligroupset:GetSetObjects()
  -- rebuild units table
  local _UnitList = {}
  for _key, _group in pairs (_allHeliGroups) do
    local _unit = _group:GetFirstUnitAlive() -- Asume that there is only one unit in the flight for players
    if _unit then
      --self:T("Unitname ".._unit:GetName().." IsAlive "..tostring(_unit:IsAlive()).." IsPlayer "..tostring(_unit:IsPlayer()))
      if _unit:IsAlive() and _unit:IsPlayer() then
        local unitName = _unit:GetName()
        _UnitList[unitName] = unitName
      end -- end isAlive
    end -- end if _unit
  end -- end for
  self.csarUnits = _UnitList

  -- build unit menus
  for _, _unitName in pairs(self.csarUnits) do
    local _unit = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT
    if _unit then
      local _group = _unit:GetGroup() -- Wrapper.Group#GROUP
      if _group then
        local groupname = _group:GetName()
        if self.addedTo[groupname] == nil then
          self.addedTo[groupname] = true
          local menuname = self.topmenuname or "CSAR"
          local _rootPath = MENU_GROUP:New(_group,menuname)
          local _rootMenu1 = MENU_GROUP_COMMAND:New(_group,"List Active CSAR",_rootPath, self._DisplayActiveSAR,self,_unitName)
          local _rootMenu2 = MENU_GROUP_COMMAND:New(_group,"Check Onboard",_rootPath, self._CheckOnboard,self,_unitName)
          local _rootMenu3 = MENU_GROUP_COMMAND:New(_group,"Request Signal Flare",_rootPath, self._SignalFlare,self,_unitName)
          local _rootMenu4 = MENU_GROUP_COMMAND:New(_group,"Request Smoke",_rootPath, self._Reqsmoke,self,_unitName)
          if self.AllowIRStrobe then
            local _rootMenu5 = MENU_GROUP_COMMAND:New(_group,"Request IR Strobe",_rootPath, self._ReqIRStrobe,self,_unitName):Refresh()
          else
            _rootMenu4:Refresh()
          end
        end
      end
    end
  end
  return self
end

--- (Internal) Return distance in meters between two coordinates.
-- @param #CSAR self
-- @param Core.Point#COORDINATE _point1 Coordinate one
-- @param Core.Point#COORDINATE _point2 Coordinate two
-- @return #number Distance in meters
function CSAR:_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) Populate table with available beacon frequencies.
-- @param #CSAR self
function CSAR:_GenerateVHFrequencies()
  self:T(self.lid .. " _GenerateVHFrequencies")

  local FreeVHFFrequencies = {}
  FreeVHFFrequencies = UTILS.GenerateVHFrequencies()
  self.FreeVHFFrequencies = FreeVHFFrequencies
  return self
end

--- (Internal) Pop frequency from prepopulated table.
-- @param #CSAR self
-- @return #number frequency
function CSAR:_GenerateADFFrequency()
  self:T(self.lid .. " _GenerateADFFrequency")
  -- get a free freq for a beacon
  if #self.FreeVHFFrequencies <= 3 then
    self.FreeVHFFrequencies = self.UsedVHFFrequencies
    self.UsedVHFFrequencies = {}
  end
  local _vhf = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies))
  return _vhf
end

--- (Internal) Function to determine clockwise direction for flares.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli The Helicopter
-- @param Wrapper.Group#GROUP _group The downed Group
-- @return #number direction
function CSAR:_GetClockDirection(_heli, _group)
  self:T(self.lid .. " _GetClockDirection")

  local _playerPosition = _heli:GetCoordinate() -- get position of helicopter
  local _targetpostions = _group:GetCoordinate() -- get position of downed pilot
  local _heading = _heli:GetHeading() -- heading
  local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions )
  local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 )
  self:T(self.lid .. " _GetClockDirection"..tostring(Angle).." "..tostring(_heading))
  local hours = 0
  local clock = 12
  if _heading and Angle then
    clock = 12
    --if angle == 0 then angle = 360 end
    clock = _heading-Angle
    hours = (clock/30)*-1
    clock = 12+hours
    clock = UTILS.Round(clock,0)
    if clock > 12 then clock = clock-12 end
  end
  return clock
end

--- (Internal) Function to add beacon to downed pilot.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP _group Group #GROUP object.
-- @param #number _freq Frequency to use
-- @param #string BeaconName Beacon Name to use
-- @return #CSAR self
function CSAR:_AddBeaconToGroup(_group, _freq, BeaconName)
  self:T(self.lid .. " _AddBeaconToGroup")
  if self.CreateRadioBeacons == false then return end
  local _group = _group

  if _group == nil then
    --return frequency to pool of available
    for _i, _current in ipairs(self.UsedVHFFrequencies) do
      if _current == _freq then
        table.insert(self.FreeVHFFrequencies, _freq)
        table.remove(self.UsedVHFFrequencies, _i)
      end
    end
    return
  end

  if _group:IsAlive() then
    local _radioUnit = _group:GetUnit(1)
    if _radioUnit then
      local name = _radioUnit:GetName()
      local Frequency = _freq -- Freq in Hertz
      --local name = _radioUnit:GetName()
      local Sound =  "l10n/DEFAULT/"..self.radioSound
      local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0}
      self:I(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z))
      trigger.action.radioTransmission(Sound, vec3, 0, true, Frequency, self.ADFRadioPwr or 500,BeaconName) -- Beacon in MP only runs for exactly 30secs straight
    end
  end

  return self
end

--- (Internal) Helper function to (re-)add beacon to downed pilot.
-- @param #CSAR self
-- @return #CSAR self
function CSAR:_RefreshRadioBeacons()
  self:T(self.lid .. " _RefreshRadioBeacons")
  if self.CreateRadioBeacons == false then return end
  if self:_CountActiveDownedPilots() > 0 then
    local PilotTable = self.downedPilots
    for _,_pilot in pairs (PilotTable) do
      self:T({_pilot.name})
      local pilot = _pilot -- #CSAR.DownedPilot
      local group = pilot.group
      local frequency = pilot.frequency or 0 -- thanks to @Thrud
      local bname = pilot.BeaconName or pilot.name..math.random(1,100000)
      --trigger.action.stopRadioTransmission(bname)
      if group and group:IsAlive() and frequency > 0 then
      --self:_AddBeaconToGroup(group,frequency,bname)
      else
        if frequency > 0 then
          trigger.action.stopRadioTransmission(bname)
        end
      end
    end
  end
  return self
end

--- (Internal) Helper function to count active downed pilots.
-- @param #CSAR self
-- @return #number Number of pilots in the field.
function CSAR:_CountActiveDownedPilots()
  self:T(self.lid .. " _CountActiveDownedPilots")
  local PilotsInFieldN = 0
  for _, _unitName in pairs(self.downedPilots) do
    self:T({_unitName.desc})
    if _unitName.alive == true then
      PilotsInFieldN = PilotsInFieldN + 1
    end
  end
  return PilotsInFieldN
end

--- (Internal) Helper to decide if we're over max limit.
-- @param #CSAR self
-- @return #boolean True or false.
function CSAR:_ReachedPilotLimit()
  self:T(self.lid .. " _ReachedPilotLimit")
  local limit = self.maxdownedpilots
  local islimited = self.limitmaxdownedpilots
  local count = self:_CountActiveDownedPilots()
  if islimited and (count >= limit) then
    if self.useFIFOLimitReplacement then
      local oldIndex  = -1
      local oldDownedPilot = nil
      for _index, _downedpilot in pairs(self.downedPilots) do
        oldIndex = _index
        oldDownedPilot = _downedpilot
        break
      end
      if oldDownedPilot then
        oldDownedPilot.group:Destroy(false)
        oldDownedPilot.alive = false
        self:_CheckDownedPilotTable()
        return false
      end
    end
    return true
  else
    return false
  end
end

--- User - Function to add onw SET_GROUP Set-up for pilot filtering and assignment.
-- Needs to be set before starting the CSAR instance.
-- @param #CSAR self
-- @param Core.Set#SET_GROUP Set The SET_GROUP object created by the mission designer/user to represent the CSAR pilot groups.
-- @return #CSAR self
function CSAR:SetOwnSetPilotGroups(Set)
  self.UserSetGroup = Set
  return self
end

------------------------------
--- FSM internal Functions ---
------------------------------

--- (Internal) Function called after Start() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onafterStart(From, Event, To)
  self:T({From, Event, To})
  self:I(self.lid .. "Started ("..self.version..")")
  -- event handler
  self:HandleEvent(EVENTS.Takeoff, self._EventHandler)
  self:HandleEvent(EVENTS.Land, self._EventHandler)
  self:HandleEvent(EVENTS.Ejection, self._EventHandler)
  self:HandleEvent(EVENTS.LandingAfterEjection, self._EventHandler) --shagrat
  self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler)
  self:HandleEvent(EVENTS.PilotDead, self._EventHandler)

  if self.UserSetGroup then
    self.allheligroupset  = self.UserSetGroup
  elseif self.allowbronco then
    local prefixes = self.csarPrefix or {}
    self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterStart()
  elseif self.useprefix then
    local prefixes = self.csarPrefix or {}
    self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart()
  else
    self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart()
  end

  self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart()

  self.staticmashes = SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart()
  self.zonemashes = SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterStart()

  --[[
  if staticmashes:Count() > 0  then
    for _,_mash in pairs(staticmashes.Set) do
      self.mash:AddObject(_mash)
    end
  end
  
  if zonemashes:Count() > 0  then
    self:T("Adding zones to self.mash SET")
    for _,_mash in pairs(zonemashes.Set) do
      self.mash:AddObject(_mash)
    end
    self:T("Objects in SET: "..self.mash:Count())
  end
  --]]

  if not self.coordinate then
    local csarhq = self.mash:GetRandom()
    if csarhq then
      self.coordinate = csarhq:GetCoordinate()
    end
  end
  if self.wetfeettemplate then
    self.usewetfeet = true
  end
  if self.useSRS then
    local path = self.SRSPath
    local modulation = self.SRSModulation
    local channel = self.SRSchannel
    self.msrs = MSRS:New(path,channel,modulation) -- Sound.SRS#MSRS
    self.msrs:SetPort(self.SRSport)
    self.msrs:SetLabel("CSAR")
    self.msrs:SetCulture(self.SRSCulture)
    self.msrs:SetCoalition(self.coalition)
    self.msrs:SetVoice(self.SRSVoice)
    self.msrs:SetGender(self.SRSGender)
    if self.SRSGPathToCredentials then
      self.msrs:SetProviderOptionsGoogle(self.SRSGPathToCredentials,self.SRSGPathToCredentials)
      self.msrs:SetProvider(MSRS.Provider.GOOGLE)
    end
    self.msrs:SetVolume(self.SRSVolume)
    self.msrs:SetLabel("CSAR")
    self.SRSQueue = MSRSQUEUE:New("CSAR") -- Sound.SRS#MSRSQUEUE
  end

  self:__Status(-10)

  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

--- (Internal) Function called before Status() event.
-- @param #CSAR self
function CSAR:_CheckDownedPilotTable()
  local pilots = self.downedPilots
  local npilots = {}

  for _ind,_entry in pairs(pilots) do
    local _group = _entry.group
    if _group:IsAlive() then
      npilots[_ind] = _entry
    else
      if _entry.alive then
        self:__KIA(1,_entry.desc)
      end
    end
  end
  self.downedPilots = npilots
  return self
end

--- (Internal) Function called before Status() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onbeforeStatus(From, Event, To)
  self:T({From, Event, To})
  -- housekeeping
  self:_AddMedevacMenuItem()

  if not self.BeaconTimer or (self.BeaconTimer and not self.BeaconTimer:IsRunning()) then
    self.BeaconTimer = TIMER:New(self._RefreshRadioBeacons,self)
    self.BeaconTimer:Start(2,self.beaconRefresher)
  end

  self:_CheckDownedPilotTable()
  for _,_sar in pairs (self.csarUnits) do
    local PilotTable = self.downedPilots
    for _,_entry in pairs (PilotTable) do
      if _entry.alive then
        local entry = _entry -- #CSAR.DownedPilot
        local name = entry.name
        local timestamp = entry.timestamp or 0
        local now = timer.getAbsTime()
        if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10.
          self:_CheckWoundedGroupStatus(_sar,name)
        end
      end
    end
  end
  return self
end

--- (Internal) Function called after Status() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  -- collect some stats
  local NumberOfSARPilots = 0
  for _, _unitName in pairs(self.csarUnits) do
    NumberOfSARPilots = NumberOfSARPilots + 1
  end

  local PilotsInFieldN = self:_CountActiveDownedPilots()

  local PilotsBoarded = 0
  for _, _unitName in pairs(self.inTransitGroups) do
    for _,_units in pairs(_unitName) do
      PilotsBoarded = PilotsBoarded + 1
    end
  end

  if self.verbose > 0 then
    local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d",
      self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots)
    self:T(text)
    if self.verbose < 2 then
      self:I(text)
    elseif self.verbose > 1 then
      self:I(text)
      local m = MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition)
    end
  end
  self:__Status(-20)
  return self
end

--- (Internal) Function called after Stop() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onafterStop(From, Event, To)
  self:T({From, Event, To})
  -- event handler
  self:UnHandleEvent(EVENTS.Takeoff)
  self:UnHandleEvent(EVENTS.Land)
  self:UnHandleEvent(EVENTS.Ejection)
  self:UnHandleEvent(EVENTS.LandingAfterEjection) -- shagrat
  self:UnHandleEvent(EVENTS.PlayerEnterUnit)
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  self:UnHandleEvent(EVENTS.PilotDead)
  self:T(self.lid .. "Stopped.")
  return self
end

--- (Internal) Function called before Approach() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string Heliname Name of the helicopter group.
-- @param #string Woundedgroupname Name of the downed pilot\'s group.
function CSAR:onbeforeApproach(From, Event, To, Heliname, Woundedgroupname)
  self:T({From, Event, To, Heliname, Woundedgroupname})
  self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname)
  return self
end

--- (Internal) Function called before Boarded() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string Heliname Name of the helicopter group.
-- @param #string Woundedgroupname Name of the downed pilot\'s group.
function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname)
  self:T({From, Event, To, Heliname, Woundedgroupname})
  self:_ScheduledSARFlight(Heliname,Woundedgroupname)
  local Unit = UNIT:FindByName(Heliname)
  if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
    local playername = Unit:GetPlayerName()
    local dropcoord = Unit: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))
            or (string.find(task.CSARPilotName,Woundedgroupname)) then
            if task.Clients:HasUniqueID(playername) then
              -- success
              task:__Success(-1)
            end
          end
        end
      end
    )
  end
  return self
end

--- (Internal) Function called before Returning() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string Heliname Name of the helicopter group.
-- @param #string Woundedgroupname Name of the downed pilot\'s group.
-- @param #boolean IsAirport True if heli has landed on an AFB (from event land).
function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname, IsAirPort)
  self:T({From, Event, To, Heliname, Woundedgroupname})
  --self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort)
  return self
end

--- (Internal) Function called before Rescued() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter.
-- @param #string HeliName Name of the helicopter group.
-- @param #number PilotsSaved Number of the saved pilots on board when landing.
function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved)
  self:T({From, Event, To, HeliName, HeliUnit})
  self.rescues = self.rescues + 1
  self.rescuedpilots = self.rescuedpilots + PilotsSaved
  local Unit = HeliUnit or UNIT:FindByName(HeliName)
  if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
    local playername = Unit:GetPlayerName()
    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
          if task.Clients:HasUniqueID(playername) then
            -- success
            task:__Success(-1)
          end
        end
      end
    )
  end
  return self
end

--- (Internal) Function called before PilotDown() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP Group Group object of the downed pilot.
-- @param #number Frequency Beacon frequency in kHz.
-- @param #string Leadername Name of the #UNIT of the downed pilot.
-- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype.
-- @param #string Playername Player name if any given. Might be nil!
function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText, Playername)
  self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText, tostring(Playername)})
  return self
end

--- (Internal) Function called before Landed() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string HeliName Name of the #UNIT which has landed.
-- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed.
function CSAR:onbeforeLanded(From, Event, To, HeliName, Airbase)
  self:T({From, Event, To, HeliName, Airbase})
  return self
end

--- On before "Save" event. Checks if io and lfs are available.
-- @param #CSAR 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 "CSAR_<alias>_Persist.csv".
function CSAR: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 #CSAR 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 "CSAR_<alias>_Persist.csv".
function CSAR: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 pilots = self.downedPilots

  --local data = "LoadedData = {\n"
  local data = "playerName,x,y,z,coalition,country,description,typeName,unitName,freq\n"
  local n = 0
  for _,_grp in pairs(pilots) do
    local DownedPilot = _grp -- Wrapper.Group#GROUP
    if DownedPilot and DownedPilot.alive then
      -- get downed pilot data for saving
      local playerName = DownedPilot.player
      local group = DownedPilot.group
      local coalition = group:GetCoalition()
      local country = group:GetCountry()
      local description = DownedPilot.desc
      local typeName = DownedPilot.typename
      local freq = DownedPilot.frequency
      local location = group:GetVec3()
      local unitName = DownedPilot.originalUnit
      local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq)

      self:I(self.lid.."Saving to CSAR File: " .. txt)

      data = data .. txt
    end
  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 #CSAR 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 "CSAR_<alias>_Persist.csv".
function CSAR: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 #CSAR 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 "CSAR_<alias>_Persist.csv".
function CSAR: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 CSAR 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)

  for _id,_entry in pairs (loadeddata) do
    local dataset = UTILS.Split(_entry,",")
    -- 1=playerName,2=x,3=y,4=z,5=coalition,6=country,7=description,8=typeName,9=unitName,10=freq\n
    local playerName = dataset[1]

    local vec3 = {}
    vec3.x = tonumber(dataset[2])
    vec3.y = tonumber(dataset[3])
    vec3.z = tonumber(dataset[4])
    local point = COORDINATE:NewFromVec3(vec3)

    local coalition = tonumber(dataset[5])
    local country = tonumber(dataset[6])
    local description = dataset[7]
    local typeName = dataset[8]
    local unitName = dataset[9]
    local freq = tonumber(dataset[10])
    
    self:_AddCsar(coalition, country, point, typeName, unitName, playerName, freq, false, description, nil)    
  end

  return self
end

--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- End Ops.CSAR
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
