TommyC81 0441acf101
Documentation fixes. (#1803)
Improve the consistency of the module intros to the most commonly used version (single dash).

Add missing module information (abbreviated where none existed previously).

Fix broken documentation links

Make module names correspond to filenames (and fix links).

Fix typos.
2022-10-19 12:20:39 +02:00

2452 lines
95 KiB
Lua

--- **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%20-%20CSAR)
--
-- ===
--
-- **Main Features:**
--
-- * MOOSE-based Helicopter CSAR Operations for Players.
--
-- ===
--
-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing)
-- @module Ops.CSAR
-- @image OPS_CSAR.jpg
-- Date: October 2022
-------------------------------------------------------------------------
--- **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.
-- @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)
--
-- ===
--
-- ![Banner Image](OPS_CSAR.jpg)
--
-- # 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".
--
-- ## 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.
-- 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
--
-- ## 2.1 Experimental Features
--
-- WARNING - Here\'ll be dragons!
-- DANGER - For this to work you need to de-sanitize your mission environment (all three entries) in <DCS root>\Scripts\MissionScripting.lua
-- Needs SRS => 1.9.6 to work (works on the **server** side of SRS)
-- mycsar.useSRS = false -- Set true to use FF\'s SRS integration
-- mycsar.SRSPath = "C:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- 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, 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.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
--
-- ## 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)
--
-- @field #CSAR
CSAR = {
ClassName = "CSAR",
verbose = 0,
lid = "",
coalition = 1,
coalitiontxt = "blue",
FreeVHFFrequencies = {},
UsedVHFFrequencies = {},
takenOff = {},
csarUnits = {}, -- table of unit names
downedPilots = {},
woundedGroups = {},
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,
allheligroupset = nil,
topmenuname = "CSAR",
ADFRadioPwr = 1000,
}
--- 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.
--- 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["AH-64D_BLK_II"] = 2
CSAR.AircraftType["Bronco-OV-10A"] = 2
--- CSAR class version.
-- @field #string version
CSAR.version="1.0.15"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DONE: SRS Integration (to be tested)
-- TODO: Maybe - add option to smoke/flare closest MASH
-- TODO: 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
--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("*", "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 0.1.8
self.allowbronco = false -- set to true to use the Bronco mod as a CSAR plane
self.ADFRadioPwr = 1000
-- 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" -- 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 = nil
self.SRSGPathToCredentials = nil
self.SRSVolume = 1.0 -- volume 0.0 to 1.0
self.SRSGender = "male" -- male or female
------------------------
--- 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.
--- 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.
--- 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.
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)
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 ""
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
-- 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)
:InitAIOnOff(pilotcacontrol)
: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
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
if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0
self:_AddBeaconToGroup(_spawnedGroup, _freq)
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)
self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc.
return self
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 #string _Point a POINT_VEC2.
-- @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 #string Point a POINT_VEC2.
-- @param #number Coalition Coalition.
-- @param #string Description (optional) Description.
-- @param #boolean addBeacon (optional) 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 casualty "CASEVAC" at Point #POINT_VEC2 for the blue coalition.
-- my_csar:SpawnCASEVAC( POINT_VEC2, 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
-- 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 initdcscoord = nil
local initcoord = nil
if _event.id == EVENTS.Ejection then
initdcscoord = _event.TgtDCSUnit:getPoint()
initcoord = COORDINATE:NewFromVec3(initdcscoord)
self:T({initdcscoord})
else
initdcscoord = _event.IniDCSUnit:getPoint()
initcoord = COORDINATE:NewFromVec3(initdcscoord)
self:T({initdcscoord})
end
--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, false, "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)
self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true)
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, false, "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.
function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage)
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'
self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
else --shagrat CASEVAC msg
local _text = string.format("Pickup Zone at %s.", _coordinatesText )
self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
end
end
for _,_heliName in pairs(self.csarUnits) do
self:_CheckWoundedGroupStatus(_heliName, _groupName)
end
-- trigger FSM event
self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText)
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)*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
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
-- have we landed close enough?
if not _heliUnit:InAir() then
if self.pilotRuntoExtractPoint == true then
if (_distance < self.extractDistance) then
local _time = self.landedStatus[_lookupKeyHeli]
if _time == nil then
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
if _distance < self.loadDistance + 5 or _distance <= 13 then
if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
return false
else
self.landedStatus[_lookupKeyHeli] = nil
self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
return true
end
end
end
else
if (_distance < self.loadDistance) then
if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
return false
else
self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
return true
end
end
end
else
local _unitsInHelicopter = self:_PilotsOnboard(_heliName)
local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()]
if _maxUnits == nil then
_maxUnits = self.max_units
end
if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then
-- DONE - make variable
if _distance < self.rescuehoverdistance then
--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
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
if _time > 0 then
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
if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
return false
else
self.hoverStatus[_lookupKeyHeli] = nil
self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
return true
end
end
_reset = false
else
self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true)
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
function CSAR:_ScheduledSARFlight(heliname,groupname, isairport)
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
return
end
if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then
if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then
self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true, true)
else
self:_RescuePilots(_heliUnit)
return
end
end
--queue up
self:__Returning(-5,heliname,_woundedGroupName, isairport)
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
self.SRSQueue:NewTransmission(_text,nil,self.msrs,nil,2)
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.
-- @return #string Coordinates as Text
function CSAR:_GetPositionOfWounded(_woundedGroup)
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
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)
local _helicoord = _heli:GetCoordinate()
local _woundcoord = _woundedGroup:GetCoordinate()
local _distance = self:_GetDistance(_helicoord, _woundcoord)
self:T({_distance = _distance})
local distancetext = ""
if _SETTINGS:IsImperial() then
distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance))
else
distancetext = string.format("%.1fkm", _distance/1000.0)
end
if _value.frequency == 0 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 = 0
if _SETTINGS:IsImperial() then
_distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance))
else
_distance = string.format("%.1fkm",_closest.distance)
end
local _msg = string.format("%s - Popping 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
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) 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.
function CSAR:_DisplayToAllSAR(_message, _side, _messagetime)
self:T(self.lid .. " _DisplayToAllSAR")
local messagetime = _messagetime or self.messageTime
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
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 = 0
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 = 0
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
-- @retunr
function CSAR:_GetClosestMASH(_heli)
self:T(self.lid .. " _GetClosestMASH")
local _mashset = self.mash -- Core.Set#SET_GROUP
local _mashes = _mashset:GetSetObjects() -- #table
local _shortestDistance = -1
local _distance = 0
local _helicoord = _heli:GetCoordinate()
local function GetCloseAirbase(coordinate,Coalition,Category)
local a=coordinate:GetVec3()
local distmin=math.huge
local airbase=nil
for DCSairbaseID, DCSairbase in pairs(world.getAirbases(Coalition)) do
local b=DCSairbase:getPoint()
local c=UTILS.VecSubstract(a,b)
local dist=UTILS.VecNorm(c)
if dist<distmin and (Category==nil or Category==DCSairbase:getDesc().category) then
distmin=dist
airbase=DCSairbase
end
end
return distmin
end
if self.allowFARPRescue then
local position = _heli:GetCoordinate()
local afb,distance = position:GetClosestAirbase(nil,self.coalition)
_shortestDistance = distance
end
for _, _mashUnit in pairs(_mashes) do
if _mashUnit and _mashUnit:IsAlive() then
local _mashcoord = _mashUnit:GetCoordinate()
_distance = self:_GetDistance(_helicoord, _mashcoord)
if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
_shortestDistance = _distance
end
end
end
if _shortestDistance ~= -1 then
return _shortestDistance
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:GetUnit(1) -- Asume that there is only one unit in the flight for players
if _unit then
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):Refresh()
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
function CSAR:_AddBeaconToGroup(_group, _freq)
self:T(self.lid .. " _AddBeaconToGroup")
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 Frequency = _freq -- Freq in Hertz
local Sound = "l10n/DEFAULT/"..self.radioSound
local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0}
trigger.action.radioTransmission(Sound, vec3, 0, false, Frequency, self.ADFRadioPwr or 1000) -- 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
-- @param #table _args Arguments
function CSAR:_RefreshRadioBeacons()
self:T(self.lid .. " _RefreshRadioBeacons")
if self:_CountActiveDownedPilots() > 0 then
local PilotTable = self.downedPilots
for _,_pilot in pairs (PilotTable) do
self:T({_pilot})
local pilot = _pilot -- #CSAR.DownedPilot
local group = pilot.group
local frequency = pilot.frequency or 0 -- thanks to @Thrud
if group and group:IsAlive() and frequency > 0 then
self:_AddBeaconToGroup(group,frequency)
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
return true
else
return false
end
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.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() -- currently only GROUP objects, maybe support STATICs also?
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)
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:SetGoogle(self.SRSGPathToCredentials)
end
self.msrs:SetVolume(self.SRSVolume)
self.msrs:SetLabel("CSAR")
self.SRSQueue = MSRSQUEUE:New("CSAR")
end
self:__Status(-10)
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.
function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText)
self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText})
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
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- End Ops.CSAR
--------------------------------------------------------------------------------------------------------------------------------------------------------------------