--- **Functional** - Autolase targets in the field. -- -- === -- -- **AUOTLASE** - Autolase targets in the field. -- -- === -- -- ## Missions: -- -- None yet. -- -- === -- -- **Main Features:** -- -- * Detect and lase contacts automatically -- * Targets are lased by threat priority order -- * Use FSM events to link functionality into your scripts -- * Easy setup -- -- === -- --- Spot on! -- -- === -- -- # 1 Autolase concept -- -- * Detect and lase contacts automatically -- * Targets are lased by threat priority order -- * Use FSM events to link functionality into your scripts -- * Set laser codes and smoke colors per Recce unit -- * Easy set-up -- -- # 2 Basic usage -- -- ## 2.2 Set up a group of Recce Units: -- -- local FoxSet = SET_GROUP:New():FilterPrefixes("Recce"):FilterCoalitions("blue"):FilterStart() -- -- ## 2.3 (Optional) Set up a group of pilots, this will drive who sees the F10 menu entry: -- -- local Pilotset = SET_CLIENT:New():FilterCoalitions("blue"):FilterActive(true):FilterStart() -- -- ## 2.4 Set up and start Autolase: -- -- local autolaser = AUTOLASE:New(FoxSet,coalition.side.BLUE,"Wolfpack",Pilotset) -- -- ## 2.5 Example - Using a fixed laser code and color for a specific Recce unit: -- -- local recce = SPAWN:New("Reaper") -- :InitDelayOff() -- :OnSpawnGroup( -- function (group) -- local unit = group:GetUnit(1) -- local name = unit:GetName() -- autolaser:SetRecceLaserCode(name,1688) -- autolaser:SetRecceSmokeColor(name,SMOKECOLOR.Red) -- end -- ) -- :InitCleanUp(60) -- :InitLimit(1,0) -- :SpawnScheduled(30,0.5) -- -- ## 2.6 Example - Inform pilots about events: -- -- autolaser:SetNotifyPilots(true) -- defaults to true, also shown if debug == true -- -- Note - message are shown to pilots in the #SET_CLIENT only if using the pilotset option, else to the coalition. -- -- -- ### Author: **applevangelist** -- @module Functional.Autolase -- @image Designation.JPG -- -- Date: 24 Oct 2021 -- Last Update: Oct 2023 -- --- Class AUTOLASE -- @type AUTOLASE -- @field #string ClassName -- @field #string lid -- @field #number verbose -- @field #string alias -- @field #boolean debug -- @field #string version -- @field Core.Set#SET_GROUP RecceSet -- @field #table LaserCodes -- @field #table playermenus -- @extends Ops.Intel#INTEL --- -- @field #AUTOLASE AUTOLASE = { ClassName = "AUTOLASE", lid = "", verbose = 0, alias = "", debug = false, } --- Laser spot info -- @type AUTOLASE.LaserSpot -- @field Core.Spot#SPOT laserspot -- @field Wrapper.Unit#UNIT lasedunit -- @field Wrapper.Unit#UNIT lasingunit -- @field #number lasercode -- @field #string location -- @field #number timestamp -- @field #string unitname -- @field #string reccename -- @field #string unittype -- @field Core.Point#COORDINATE coordinate --- AUTOLASE class version. -- @field #string version AUTOLASE.version = "0.1.22" ------------------------------------------------------------------- -- Begin Functional.Autolase.lua ------------------------------------------------------------------- --- Constructor for a new Autolase instance. -- @param #AUTOLASE self -- @param Core.Set#SET_GROUP RecceSet Set of detecting and lasing units -- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". -- @param #string Alias (Optional) An alias how this object is called in the logs etc. -- @param Core.Set#SET_CLIENT PilotSet (Optional) Set of clients for precision bombing, steering menu creation. Leave nil for a coalition-wide F10 entry and display. -- @return #AUTOLASE self function AUTOLASE:New(RecceSet, Coalition, Alias, PilotSet) BASE:T({RecceSet, Coalition, Alias, PilotSet}) -- Inherit everything from BASE class. local self=BASE:Inherit(self, BASE:New()) -- #AUTOLASE if Coalition and type(Coalition)=="string" then if Coalition=="blue" then self.coalition=coalition.side.BLUE elseif Coalition=="red" then self.coalition=coalition.side.RED elseif Coalition=="neutral" then self.coalition=coalition.side.NEUTRAL else self:E("ERROR: Unknown coalition in AUTOLASE!") end end -- Set alias. if Alias then self.alias=tostring(Alias) else self.alias="Lion" if self.coalition then if self.coalition==coalition.side.RED then self.alias="Wolf" elseif self.coalition==coalition.side.BLUE then self.alias="Fox" end end end -- inherit from INTEL local self=BASE:Inherit(self, INTEL:New(RecceSet, Coalition, Alias)) -- #AUTOLASE self.RecceSet = RecceSet self.DetectVisual = true self.DetectOptical = true self.DetectRadar = true self.DetectIRST = true self.DetectRWR = true self.DetectDLINK = true self.LaserCodes = UTILS.GenerateLaserCodes() self.LaseDistance = 5000 self.LaseDuration = 300 self.GroupsByThreat = {} self.UnitsByThreat = {} self.RecceNames = {} self.RecceLaserCode = {} self.RecceSmokeColor = {} self.RecceUnitNames= {} self.maxlasing = 4 self.CurrentLasing = {} self.lasingindex = 0 self.deadunitnotes = {} self.usepilotset = false self.reporttimeshort = 10 self.reporttimelong = 30 self.smoketargets = false self.smokecolor = SMOKECOLOR.Red self.notifypilots = true self.targetsperrecce = {} self.RecceUnits = {} self.forcecooldown = true self.cooldowntime = 60 self.useSRS = false self.SRSPath = "" self.SRSFreq = 251 self.SRSMod = radio.modulation.AM self.NoMenus = false self.minthreatlevel = 0 self.blacklistattributes = {} self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes self.playermenus = {} -- Set some string id for output to DCS.log file. self.lid=string.format("AUTOLASE %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("*", "Monitor", "*") -- Start FSM self:AddTransition("*", "Lasing", "*") -- Lasing target self:AddTransition("*", "TargetLost", "*") -- Lost target self:AddTransition("*", "TargetDestroyed", "*") -- Target destroyed self:AddTransition("*", "RecceKIA", "*") -- Recce KIA self:AddTransition("*", "LaserTimeout", "*") -- Laser timed out self:AddTransition("*", "Cancel", "*") -- Stop Autolase -- Menu Entry if PilotSet then self.usepilotset = true self.pilotset = PilotSet self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) --self:SetPilotMenu() end --self.SetPilotMenu() self:SetClusterAnalysis(false, false) self:__Start(2) self:__Monitor(math.random(5,10)) return self ------------------------ --- Pseudo Functions --- ------------------------ --- Triggers the FSM event "Monitor". -- @function [parent=#AUTOLASE] Status -- @param #AUTOLASE self --- Triggers the FSM event "Monitor" after a delay. -- @function [parent=#AUTOLASE] __Status -- @param #AUTOLASE self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Cancel". -- @function [parent=#AUTOLASE] Cancel -- @param #AUTOLASE self --- Triggers the FSM event "Cancel" after a delay. -- @function [parent=#AUTOLASE] __Cancel -- @param #AUTOLASE self -- @param #number delay Delay in seconds. --- On After "RecceKIA" event. -- @function [parent=#AUTOLASE] OnAfterRecceKIA -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string RecceName The lost Recce --- On After "TargetDestroyed" event. -- @function [parent=#AUTOLASE] OnAfterTargetDestroyed -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string UnitName The destroyed unit\'s name -- @param #string RecceName The Recce name lasing --- On After "TargetLost" event. -- @function [parent=#AUTOLASE] OnAfterTargetLost -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string UnitName The lost unit\'s name -- @param #string RecceName The Recce name lasing --- On After "LaserTimeout" event. -- @function [parent=#AUTOLASE] OnAfterLaserTimeout -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string UnitName The lost unit\'s name -- @param #string RecceName The Recce name lasing --- On After "Lasing" event. -- @function [parent=#AUTOLASE] OnAfterLasing -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table end ------------------------------------------------------------------- -- Helper Functions ------------------------------------------------------------------- --- [User] Set a table of possible laser codes. -- Each new RECCE can select a code from this table, default is { 1688, 1130, 4785, 6547, 1465, 4578 } . -- @param #AUTOLASE self -- @param #list<#number> LaserCodes -- @return #AUTOLASE function AUTOLASE:SetLaserCodes( LaserCodes ) self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes } return self end --- (Internal) Function to set pilot menu. -- @param #AUTOLASE self -- @return #AUTOLASE self function AUTOLASE:SetPilotMenu() if self.usepilotset then local pilottable = self.pilotset:GetSetObjects() or {} for _,_unit in pairs (pilottable) do local Unit = _unit -- Wrapper.Unit#UNIT if Unit and Unit:IsAlive() then local Group = Unit:GetGroup() local unitname = Unit:GetName() if self.playermenus[unitname] then self.playermenus[unitname]:Remove() end local lasetopm = MENU_GROUP:New(Group,"Autolase",nil) self.playermenus[unitname] = lasetopm local lasemenu = MENU_GROUP_COMMAND:New(Group,"Status",lasetopm,self.ShowStatus,self,Group,Unit) local smoke = (self.smoketargets == true) and "off" or "on" local smoketext = string.format("Switch smoke targets to %s",smoke) local smokemenu = MENU_GROUP_COMMAND:New(Group,smoketext,lasetopm,self.SetSmokeTargets,self,(not self.smoketargets)) for _,_grp in pairs(self.RecceSet.Set) do local grp = _grp -- Wrapper.Group#GROUP local unit = grp:GetUnit(1) --local name = grp:GetName() if unit and unit:IsAlive() then local name = unit:GetName() local mname = string.gsub(name,".%d+.%d+$","") local code = self:GetLaserCode(name) local unittop = MENU_GROUP:New(Group,"Change laser code for "..mname,lasetopm) for _,_code in pairs(self.LaserCodes) do local text = tostring(_code) if _code == code then text = text.."(*)" end local changemenu = MENU_GROUP_COMMAND:New(Group,text,unittop,self.SetRecceLaserCode,self,name,_code,true) end end end --lasemenu:Refresh() end end else if not self.NoMenus then self.Menu = MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self) end end return self end --- (Internal) Event function for new pilots. -- @param #AUTOLASE self -- @param Core.Event#EVENTDATA EventData -- @return #AUTOLASE self function AUTOLASE:_EventHandler(EventData) self:SetPilotMenu() return self end --- (User) Set minimum threat level for target selection, can be 0 (lowest) to 10 (highest). -- @param #AUTOLASE self -- @param #number Level Level used for filtering, defaults to 0. SAM systems and manpads have level 7 to 10, AAA level 6, MTBs and armoured vehicles level 3 to 5, APC, Artillery, Infantry and EWR level 1 to 2. -- @return #AUTOLASE self -- @usage Filter for level 3 and above: -- `myautolase:SetMinThreatLevel(3)` function AUTOLASE:SetMinThreatLevel(Level) local level = Level or 0 if level < 0 or level > 10 then level = 0 end self.minthreatlevel = level return self end --- (User) Set list of #UNIT level attributes that won't be lased. For list of attributes see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes) and [GitHub](https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB) -- @param #AUTOLASE self -- @param #table Attributes Table of #string attributes to blacklist. Can be handed over as a single #string. -- @return #AUTOLASE self -- @usage To exclude e.g. manpads from being lased: -- -- `myautolase:AddBlackListAttributes("MANPADS")` -- -- To exclude trucks and artillery: -- -- `myautolase:AddBlackListAttributes({"Trucks","Artillery"})` -- function AUTOLASE:AddBlackListAttributes(Attributes) local attributes = Attributes if type(attributes) ~= "table" then attributes = {attributes} end for _,_attr in pairs(attributes) do table.insert(self.blacklistattributes,_attr) end return self end --- (Internal) Function to get a laser code by recce name -- @param #AUTOLASE self -- @param #string RecceName Unit(!) name of the Recce -- @return #AUTOLASE self function AUTOLASE:GetLaserCode(RecceName) local code = 1688 if self.RecceLaserCode[RecceName] == nil then code = self.LaserCodes[math.random(#self.LaserCodes)] self.RecceLaserCode[RecceName] = code else code = self.RecceLaserCode[RecceName] end return code end --- (Internal) Function to get a smoke color by recce name -- @param #AUTOLASE self -- @param #string RecceName Unit(!) name of the Recce -- @return #AUTOLASE self function AUTOLASE:GetSmokeColor(RecceName) local color = self.smokecolor if self.RecceSmokeColor[RecceName] == nil then self.RecceSmokeColor[RecceName] = color else color = self.RecceSmokeColor[RecceName] end return color end --- (User) Function enable sending messages via SRS. -- @param #AUTOLASE self -- @param #boolean OnOff Switch usage on and off -- @param #string Path Path to SRS directory, e.g. C:\\Program Files\\DCS-SimpleRadio-Standalone -- @param #number Frequency Frequency to send, e.g. 243 -- @param #number Modulation Modulation i.e. radio.modulation.AM or radio.modulation.FM -- @param #string Label (Optional) Short label to be used on the SRS Client Overlay -- @param #string Gender (Optional) Defaults to "male" -- @param #string Culture (Optional) Defaults to "en-US" -- @param #number Port (Optional) Defaults to 5002 -- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. -- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. -- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest) -- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS -- @return #AUTOLASE self function AUTOLASE:SetUsingSRS(OnOff,Path,Frequency,Modulation,Label,Gender,Culture,Port,Voice,Volume,PathToGoogleKey) if OnOff then self.useSRS = true self.SRSPath = Path or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" self.SRSFreq = Frequency or 271 self.SRSMod = Modulation or radio.modulation.AM self.Gender = Gender or MSRS.gender or "male" self.Culture = Culture or MSRS.culture or "en-US" self.Port = Port or MSRS.port or 5002 self.Voice = Voice self.PathToGoogleKey = PathToGoogleKey self.Volume = Volume or 1.0 self.Label = Label -- set up SRS self.SRS = MSRS:New(self.SRSPath,self.SRSFreq,self.SRSMod) self.SRS:SetCoalition(self.coalition) self.SRS:SetLabel(self.MenuName or self.Name) self.SRS:SetGender(self.Gender) self.SRS:SetCulture(self.Culture) self.SRS:SetPort(self.Port) self.SRS:SetVoice(self.Voice) self.SRS:SetCoalition(self.coalition) self.SRS:SetVolume(self.Volume) if self.PathToGoogleKey then self.SRS:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey) self.SRS:SetProvider(MSRS.Provider.GOOGLE) end self.SRSQueue = MSRSQUEUE:New(self.alias) else self.useSRS = false self.SRS= nil self.SRSQueue = nil end return self end --- (User) Function set max lasing targets -- @param #AUTOLASE self -- @param #number Number Max number of targets to lase at once -- @return #AUTOLASE self function AUTOLASE:SetMaxLasingTargets(Number) self.maxlasing = Number or 4 return self end --- (Internal) Function set notify pilots on events -- @param #AUTOLASE self -- @param #boolean OnOff Switch messaging on (true) or off (false) -- @return #AUTOLASE self function AUTOLASE:SetNotifyPilots(OnOff) self.notifypilots = OnOff and true return self end --- (User) Function to set a specific code to a Recce. -- @param #AUTOLASE self -- @param #string RecceName (Unit!) Name of the Recce -- @param #number Code The lase code -- @param #boolean Refresh If true, refresh menu entries -- @return #AUTOLASE self function AUTOLASE:SetRecceLaserCode(RecceName, Code, Refresh) local code = Code or 1688 self.RecceLaserCode[RecceName] = code if Refresh then self:SetPilotMenu() if self.notifypilots then if string.find(RecceName,"#") then RecceName = string.match(RecceName,"^(.*)#") end self:NotifyPilots(string.format("Code for %s set to: %d",RecceName,Code),15) end end return self end --- (User) Function to set a specific smoke color for a Recce. -- @param #AUTOLASE self -- @param #string RecceName (Unit!) Name of the Recce -- @param #number Color The color, e.g. SMOKECOLOR.Red, SMOKECOLOR.Green etc -- @return #AUTOLASE self function AUTOLASE:SetRecceSmokeColor(RecceName, Color) local color = Color or self.smokecolor self.RecceSmokeColor[RecceName] = color return self end --- (User) Function to force laser cooldown and cool down time -- @param #AUTOLASE self -- @param #boolean OnOff Switch cool down on (true) or off (false) - defaults to true -- @param #number Seconds Number of seconds for cooldown - dafaults to 60 seconds -- @return #AUTOLASE self function AUTOLASE:SetLaserCoolDown(OnOff, Seconds) self.forcecooldown = OnOff and true self.cooldowntime = Seconds or 60 return self end --- (User) Function to set message show times. -- @param #AUTOLASE self -- @param #number long Longer show time -- @param #number short Shorter show time -- @return #AUTOLASE self function AUTOLASE:SetReportingTimes(long, short) self.reporttimeshort = short or 10 self.reporttimelong = long or 30 return self end --- (User) Function to set lasing distance in meters and duration in seconds -- @param #AUTOLASE self -- @param #number Distance (Max) distance for lasing in meters - default 5000 meters -- @param #number Duration (Max) duration for lasing in seconds - default 300 secs -- @return #AUTOLASE self function AUTOLASE:SetLasingParameters(Distance, Duration) self.LaseDistance = Distance or 5000 self.LaseDuration = Duration or 300 return self end --- (User) Function to set smoking of targets. -- @param #AUTOLASE self -- @param #boolean OnOff Switch smoking on or off -- @param #number Color Smokecolor, e.g. SMOKECOLOR.Red -- @return #AUTOLASE self function AUTOLASE:SetSmokeTargets(OnOff,Color) self.smoketargets = OnOff self.smokecolor = Color or SMOKECOLOR.Red local smktxt = OnOff == true and "on" or "off" local Message = "Smoking targets is now "..smktxt.."!" self:NotifyPilots(Message,10) return self end --- (Internal) Function to calculate line of sight. -- @param #AUTOLASE self -- @param Wrapper.Unit#UNIT Unit -- @return #number LOS Line of sight in meters function AUTOLASE:GetLosFromUnit(Unit) local lasedistance = self.LaseDistance local unitheight = Unit:GetHeight() local coord = Unit:GetCoordinate() local landheight = coord:GetLandHeight() local asl = unitheight - landheight if asl > 100 then local absquare = lasedistance^2+asl^2 lasedistance = math.sqrt(absquare) end return lasedistance end --- (Internal) Function to check on lased targets. -- @param #AUTOLASE self -- @return #AUTOLASE self function AUTOLASE:CleanCurrentLasing() local lasingtable = self.CurrentLasing local newtable = {} local newreccecount = {} local lasing = 0 for _ind,_entry in pairs(lasingtable) do local entry = _entry -- #AUTOLASE.LaserSpot if not newreccecount[entry.reccename] then newreccecount[entry.reccename] = 0 end end for _,_recce in pairs (self.RecceSet:GetSetObjects()) do local recce = _recce --Wrapper.Group#GROUP if recce and recce:IsAlive() then local unit = recce:GetUnit(1) local name = unit:GetName() if not self.RecceUnits[name] then self.RecceUnits[name] = { name=name, unit=unit, cooldown = false, timestamp = timer.getAbsTime() } end end end for _ind,_entry in pairs(lasingtable) do local entry = _entry -- #AUTOLASE.LaserSpot local valid = 0 local reccedead = false local unitdead = false local lostsight = false local timeout = false local Tnow = timer.getAbsTime() -- check recce dead local recce = entry.lasingunit if recce and recce:IsAlive() then valid = valid + 1 else reccedead = true self:__RecceKIA(2,entry.reccename) end -- check entry dead local unit = entry.lasedunit if unit and unit:IsAlive() == true then valid = valid + 1 else unitdead = true if not self.deadunitnotes[entry.unitname] then self.deadunitnotes[entry.unitname] = true self:__TargetDestroyed(2,entry.unitname,entry.reccename) end end -- check entry out of sight if not reccedead and not unitdead then if self:CanLase(recce,unit) then valid = valid + 1 else lostsight = true entry.laserspot:LaseOff() self:__TargetLost(2,entry.unitname,entry.reccename) end end -- check timed out local timestamp = entry.timestamp if Tnow - timestamp < self.LaseDuration and not lostsight then valid = valid + 1 else timeout = true entry.laserspot:LaseOff() self.RecceUnits[entry.reccename].cooldown = true self.RecceUnits[entry.reccename].timestamp = timer.getAbsTime() if not lostsight then self:__LaserTimeout(2,entry.unitname,entry.reccename) end end if valid == 4 then self.lasingindex = self.lasingindex + 1 newtable[self.lasingindex] = entry newreccecount[entry.reccename] = newreccecount[entry.reccename] + 1 lasing = lasing + 1 end end self.CurrentLasing = newtable self.targetsperrecce = newreccecount return lasing end --- (Internal) Function to show status. -- @param #AUTOLASE self -- @param Wrapper.Group#GROUP Group (Optional) show to a certain group -- @param Wrapper.Unit#UNIT Unit (Optional) show to a certain unit -- @return #AUTOLASE self function AUTOLASE:ShowStatus(Group,Unit) local report = REPORT:New("Autolase") local reccetable = self.RecceSet:GetSetObjects() for _,_recce in pairs(reccetable) do if _recce and _recce:IsAlive() then local unit = _recce:GetUnit(1) local name = unit:GetName() if string.find(name,"#") then name = string.match(name,"^(.*)#") end local code = self:GetLaserCode(unit:GetName()) report:Add(string.format("Recce %s has code %d",name,code)) end end local lines = 0 for _ind,_entry in pairs(self.CurrentLasing) do local entry = _entry -- #AUTOLASE.LaserSpot local reccename = entry.reccename if string.find(reccename,"#") then reccename = string.match(reccename,"^(.*)#") end local typename = entry.unittype local code = entry.lasercode local locationstring = entry.location local playername = nil if Unit and Unit:IsAlive() then playername = Unit:GetPlayerName() elseif Group and Group:IsAlive() then playername = Group:GetPlayerName() end if playername then local settings = _DATABASE:GetPlayerSettings(playername) if settings then self:I("Get Settings ok!") if settings:IsA2G_MGRS() then locationstring = entry.coordinate:ToStringMGRS(settings) elseif settings:IsA2G_LL_DMS() then locationstring = entry.coordinate:ToStringLLDMS(settings) elseif settings:IsA2G_BR() then locationstring = entry.coordinate:ToStringBR(Group:GetCoordinate() or Unit:GetCoordinate(),settings) end end end local text = string.format("%s lasing %s code %d\nat %s",reccename,typename,code,locationstring) report:Add(text) lines = lines + 1 end if lines == 0 then report:Add("No targets!") end local reporttime = self.reporttimelong if lines == 0 then reporttime = self.reporttimeshort end if Unit and Unit:IsAlive() then local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToUnit(Unit) elseif Group and Group:IsAlive() then local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToGroup(Group) else local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToCoalition(self.coalition) end return self end --- (Internal) Function to show messages. -- @param #AUTOLASE self -- @param #string Message The message to be sent -- @param #number Duration Duration in seconds -- @return #AUTOLASE self function AUTOLASE:NotifyPilots(Message,Duration) if self.usepilotset then local pilotset = self.pilotset:GetSetObjects() --#table for _,_pilot in pairs(pilotset) do local pilot = _pilot -- Wrapper.Unit#UNIT if pilot and pilot:IsAlive() then local Group = pilot:GetGroup() local m = MESSAGE:New(Message,Duration,"Autolase"):ToGroup(Group) end end elseif not self.debug then local m = MESSAGE:New(Message,Duration,"Autolase"):ToCoalition(self.coalition) else local m = MESSAGE:New(Message,Duration,"Autolase"):ToAll() end if self.debug then self:I(Message) end return self end --- (User) Send messages via SRS. -- @param #AUTOLASE self -- @param #string Message The (short!) message to be sent, e.g. "Lasing target!" -- @return #AUTOLASE self -- @usage Step 1 - set up the radio basics **once** with -- my_autolase:SetUsingSRS(true,"C:\\path\\SRS-Folder",251,radio.modulation.AM) -- Step 2 - send a message, e.g. -- function my_autolase:OnAfterLasing(From, Event, To, LaserSpot) -- my_autolase:NotifyPilotsWithSRS("Reaper lasing new target!") -- end function AUTOLASE:NotifyPilotsWithSRS(Message) if self.useSRS then self.SRSQueue:NewTransmission(Message,nil,self.SRS,nil,2) end if self.debug then self:I(Message) end return self end --- (Internal) Function to check if a unit is already lased. -- @param #AUTOLASE self -- @param #string unitname Name of the unit to check -- @return #boolean outcome True or false function AUTOLASE:CheckIsLased(unitname) local outcome = false for _,_laserspot in pairs(self.CurrentLasing) do local spot = _laserspot -- #AUTOLASE.LaserSpot if spot.unitname == unitname then outcome = true break end end return outcome end --- (Internal) Function to check if a unit can be lased. -- @param #AUTOLASE self -- @param Wrapper.Unit#UNIT Recce The Recce #UNIT -- @param Wrapper.Unit#UNIT Unit The lased #UNIT -- @return #boolean outcome True or false function AUTOLASE:CanLase(Recce,Unit) local function HasNoBlackListAttribute(Unit) local nogos = self.blacklistattributes or {} local having = true local unit = Unit -- Wrapper.Unit#UNIT for _,_attribute in pairs (nogos) do if unit:HasAttribute(_attribute) then having = false break end end return having end local canlase = false -- cooldown? if Recce and Recce:IsAlive() == true then local name = Recce:GetName() local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown if cooldown then local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp if Tdiff < self.cooldowntime then return false else self.RecceUnits[name].cooldown = false end end -- calculate LOS local reccecoord = Recce:GetCoordinate() local unitcoord = Unit:GetCoordinate() local islos = reccecoord:IsLOS(unitcoord,2.5) -- calculate distance local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) local lasedistance = self:GetLosFromUnit(Recce) if distance <= lasedistance and islos and HasNoBlackListAttribute(Unit) then canlase = true end end return canlase end ------------------------------------------------------------------- -- FSM Functions ------------------------------------------------------------------- --- (Internal) FSM Function for monitoring -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @return #AUTOLASE self function AUTOLASE:onbeforeMonitor(From, Event, To) self:T({From, Event, To}) -- Check if group has detected any units. self:UpdateIntel() return self end --- (Internal) FSM Function for monitoring -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @return #AUTOLASE self function AUTOLASE:onafterMonitor(From, Event, To) self:T({From, Event, To}) -- Housekeeping local countlases = self:CleanCurrentLasing() self:SetPilotMenu() local detecteditems = self.Contacts or {} -- #table of Ops.Intelligence#INTEL.Contact local groupsbythreat = {} local report = REPORT:New("Detections") local lines = 0 for _,_contact in pairs(detecteditems) do local contact = _contact -- Ops.Intelligence#INTEL.Contact local grp = contact.group local coord = contact.position local reccename = contact.recce or "none" local threat = contact.threatlevel or 0 local reccegrp = UNIT:FindByName(reccename) if reccegrp then local reccecoord = reccegrp:GetCoordinate() local distance = math.floor(reccecoord:Get3DDistance(coord)) local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, math.floor(distance/1000), contact.threatlevel) report:Add(text) self:T(text) if self.debug then self:I(text) end lines = lines + 1 -- sort out groups beyond sight local lasedistance = self:GetLosFromUnit(reccegrp) if grp:IsGround() and lasedistance >= distance and threat >= self.minthreatlevel then table.insert(groupsbythreat,{contact.group,contact.threatlevel}) self.RecceNames[contact.groupname] = contact.recce end end end self.GroupsByThreat = groupsbythreat if self.verbose > 2 and lines > 0 then local m=MESSAGE:New(report:Text(),self.reporttimeshort,"Autolase"):ToAll() end table.sort(self.GroupsByThreat, function(a,b) local aNum = a[2] -- Coin value of a local bNum = b[2] -- Coin value of b return aNum > bNum -- Return their comparisons, < for ascending, > for descending end) -- build table of Units local unitsbythreat = {} for _,_entry in pairs(self.GroupsByThreat) do local group = _entry[1] -- Wrapper.Group#GROUP if group and group:IsAlive() then local units = group:GetUnits() local reccename = self.RecceNames[group:GetName()] for _,_unit in pairs(units) do local unit = _unit -- Wrapper.Unit#UNIT if unit and unit:IsAlive() then local threat = unit:GetThreatLevel() local coord = unit:GetCoordinate() if threat >= self.minthreatlevel then local unitname = unit:GetName() -- prefer radar units if unit:HasAttribute("RADAR_BAND1_FOR_ARM") or unit:HasAttribute("RADAR_BAND2_FOR_ARM") or unit:HasAttribute("Optical Tracker") then threat = 11 end table.insert(unitsbythreat,{unit,threat}) self.RecceUnitNames[unitname] = reccename end end end end end self.UnitsByThreat = unitsbythreat table.sort(self.UnitsByThreat, function(a,b) local aNum = a[2] -- Coin value of a local bNum = b[2] -- Coin value of b return aNum > bNum -- Return their comparisons, < for ascending, > for descending end) local unitreport = REPORT:New("Detected Units") local lines = 0 for _,_entry in pairs(self.UnitsByThreat) do local threat = _entry[2] local unit = _entry[1] local unitname = unit:GetName() local text = string.format("Unit %s | Threatlevel %d | Detected by %s",unitname,threat,self.RecceUnitNames[unitname]) unitreport:Add(text) lines = lines + 1 self:T(text) if self.debug then self:I(text) end end if self.verbose > 2 and lines > 0 then local m=MESSAGE:New(unitreport:Text(),self.reporttimeshort,"Autolase"):ToAll() end for _,_detectingunit in pairs(self.RecceUnits) do local reccename = _detectingunit.name local recce = _detectingunit.unit local reccecount = self.targetsperrecce[reccename] or 0 local targets = 0 for _,_entry in pairs(self.UnitsByThreat) do local unit = _entry[1] -- Wrapper.Unit#UNIT local unitname = unit:GetName() local canlase = self:CanLase(recce,unit) if targets+reccecount < self.maxlasing and not self:CheckIsLased(unitname) and unit:IsAlive() and canlase then targets = targets + 1 local code = self:GetLaserCode(reccename) local spot = SPOT:New(recce) spot:LaseOn(unit,code,self.LaseDuration) local locationstring = unit:GetCoordinate():ToStringLLDDM() if _SETTINGS:IsA2G_MGRS() then local precision = _SETTINGS:GetMGRS_Accuracy() local settings = {} settings.MGRS_Accuracy = precision locationstring = unit:GetCoordinate():ToStringMGRS(settings) elseif _SETTINGS:IsA2G_LL_DMS() then locationstring = unit:GetCoordinate():ToStringLLDMS(_SETTINGS) elseif _SETTINGS:IsA2G_BR() then locationstring = unit:GetCoordinate():ToStringBULLS(self.coalition,_SETTINGS) end local laserspot = { -- #AUTOLASE.LaserSpot laserspot = spot, lasedunit = unit, lasingunit = recce, lasercode = code, location = locationstring, timestamp = timer.getAbsTime(), unitname = unitname, reccename = reccename, unittype = unit:GetTypeName(), coordinate = unit:GetCoordinate(), } if self.smoketargets then local coord = unit:GetCoordinate() local color = self:GetSmokeColor(reccename) coord:Smoke(color) end self.lasingindex = self.lasingindex + 1 self.CurrentLasing[self.lasingindex] = laserspot self:__Lasing(2,laserspot) end end end self:__Monitor(-30) return self end --- (Internal) FSM Function onbeforeRecceKIA -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string RecceName The lost Recce -- @return #AUTOLASE self function AUTOLASE:onbeforeRecceKIA(From,Event,To,RecceName) self:T({From, Event, To, RecceName}) if self.notifypilots or self.debug then if string.find(RecceName,"#") then RecceName = string.match(RecceName,"^(.*)#") end local text = string.format("Recce %s KIA!",RecceName) self:NotifyPilots(text,self.reporttimeshort) end return self end --- (Internal) FSM Function onbeforeTargetDestroyed -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string UnitName The destroyed unit\'s name -- @param #string RecceName The Recce name lasing -- @return #AUTOLASE self function AUTOLASE:onbeforeTargetDestroyed(From,Event,To,UnitName,RecceName) self:T({From, Event, To, UnitName, RecceName}) if self.notifypilots or self.debug then local text = string.format("Unit %s destroyed! Good job!",UnitName) self:NotifyPilots(text,self.reporttimeshort) end return self end --- (Internal) FSM Function onbeforeTargetLost -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string UnitName The lost unit\'s name -- @param #string RecceName The Recce name lasing -- @return #AUTOLASE self function AUTOLASE:onbeforeTargetLost(From,Event,To,UnitName,RecceName) self:T({From, Event, To, UnitName,RecceName}) if self.notifypilots or self.debug then if string.find(RecceName,"#") then RecceName = string.match(RecceName,"^(.*)#") end local text = string.format("%s lost sight of unit %s.",RecceName,UnitName) self:NotifyPilots(text,self.reporttimeshort) end return self end --- (Internal) FSM Function onbeforeLaserTimeout -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param #string UnitName The lost unit\'s name -- @param #string RecceName The Recce name lasing -- @return #AUTOLASE self function AUTOLASE:onbeforeLaserTimeout(From,Event,To,UnitName,RecceName) self:T({From, Event, To, UnitName,RecceName}) if self.notifypilots or self.debug then if string.find(RecceName,"#") then RecceName = string.match(RecceName,"^(.*)#") end local text = string.format("%s laser timeout on unit %s.",RecceName,UnitName) self:NotifyPilots(text,self.reporttimeshort) end return self end --- (Internal) FSM Function onbeforeLasing -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table -- @return #AUTOLASE self function AUTOLASE:onbeforeLasing(From,Event,To,LaserSpot) self:T({From, Event, To, LaserSpot.unittype}) if self.notifypilots or self.debug then local laserspot = LaserSpot -- #AUTOLASE.LaserSpot local name = laserspot.reccename if string.find(name,"#") then name = string.match(name,"^(.*)#") end local text = string.format("%s is lasing %s code %d\nat %s",name,laserspot.unittype,laserspot.lasercode,laserspot.location) self:NotifyPilots(text,self.reporttimeshort+5) end return self end --- (Internal) FSM Function onbeforeCancel -- @param #AUTOLASE self -- @param #string From The from state -- @param #string Event The event -- @param #string To The to state -- @return #AUTOLASE self function AUTOLASE:onbeforeCancel(From,Event,To) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:__Stop(2) return self end ------------------------------------------------------------------- -- End Functional.Autolase.lua -------------------------------------------------------------------