diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua new file mode 100644 index 000000000..f826b07ac --- /dev/null +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -0,0 +1,679 @@ +--- **Functional** - Autolase targets in the field. +-- +-- **Main Features:** +-- +-- * Detect and lase contacts automaticallyt +-- * Targets are lased by threat priority order +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +-- === +-- +-- ### Author: **applevangelist** +-- @module Functional.Autolase +-- @image Designation.JPG +-- +-- Date: Oct 2021 + +--- +--- Class AUTOLASE +-- @type AUTOLASE +-- @field #string ClassName +-- @field #string lid +-- @field #number verbose +-- @field #string alias +-- @field #boolean debug +-- @field #string version +-- @extends Ops.Intel#INTEL + +--- 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 +-- * Easy set-up +-- * Targets are lased by threat priority order +-- +-- # 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 for a specific Recce unit: +-- +-- local recce = SPAWN:New("Reaper") +-- :InitDelayOff() +-- :OnSpawnGroup( +-- function (group) +-- local name = group:GetName() +-- autolaser:SetRecceLaserCode(name,1688) +-- end +-- ) +-- :InitCleanUp(60) +-- :InitLimit(1,0) +-- :SpawnScheduled(30,0.5) +-- +-- ## 2.6 Example - Inform pilots about a new target: +-- +-- function autolaser:OnAfterLasing(From,Event,To,LaserSpot) +-- local laserspot = LaserSpot -- #AUTOLASE.LaserSpot +-- local text = string.format("%s is lasing %s code %d\nat %s",laserspot.reccename,laserspot.unittype,laserspot.lasercode,laserspot.location) +-- local m = MESSAGE:New(text,15,"Autolase"):ToAll() +-- return self +-- end +-- +-- @field #AUTOLASE +AUTOLASE = { + ClassName = "AUTOLASE", + lid = "", + verbose = 2, + 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 + +--- AUTOLASE class version. +-- @field #string version +AUTOLASE.version = "0.0.1" + +------------------------------------------------------------------- +-- 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.DetectVisual = true + self.DetectOptical = true + self.DetectRadar = false + self.DetectIRST = true + self.DetectRWR = false + self.DetectDLINK = true + self.LaserCodes = UTILS.GenerateLaserCodes() + self.LaseDistance = 4000 + self.LaseDuration = 120 + self.GroupsByThreat = {} + self.UnitsByThreat = {} + self.RecceNames = {} + self.RecceLaserCode = {} + 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 + + -- 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 + + -- Menu Entry + if not PilotSet then + self.Menu = MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self) + else + self.usepilotset = true + self.pilotset = PilotSet + self:HandleEvent(EVENTS.PlayerEnterAircraft) + self:SetPilotMenu() + end + + self:SetClusterAnalysis(false, false) + + self:__Start(2) + self:__Monitor(-5) + + return self + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Monitor". + -- @function [parent=#INTEL] Status + -- @param #INTEL self + + --- Triggers the FSM event "Monitor" after a delay. + -- @function [parent=#INTEL] __Status + -- @param #INTEL 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 +------------------------------------------------------------------- + +--- (Internal) Function to set pilot menu. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:SetPilotMenu() + 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 lasemenu = MENU_GROUP_COMMAND:New(Group,"Autolase",nil,self.ShowStatus,self,Group) + lasemenu:Refresh() + end + end + return self +end + +--- (Internal) Event function for new pilots. +-- @param #AUTOLASE self +-- @param Core.Event#EVENTDATA EventData +-- @return #AUTOLASE self +function AUTOLASE:OnEventPlayerEnterAircraft(EventData) + self:SetPilotMenu() + return self +end + +--- Function to get a laser code by recce name +-- @param #AUTOLASE self +-- @param #string RecceName 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 + +--- 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 + +--- (User) Function to set a specific code to a Recce. +-- @param #AUTOLASE self +-- @param #string RecceName Name of the Recce +-- @param #number Code The lase code +-- @return #AUTOLASE self +function AUTOLASE:SetRecceLaserCode(RecceName, Code) + local code = Code or 1688 + self.RecceLaserCode[RecceName] = code + 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 +-- @param #number Duration (Max) duration for lasing in seconds +-- @return #AUTOLASE self +function AUTOLASE:SetLasingParameters(Distance, Duration) + self.LaseDistance = distance or 4000 + self.LaseDuration = duration or 120 + 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 + return self +end + +--- (Internal) Function to check on lased targets. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:CleanCurrentLasing() + local lasingtable = self.CurrentLasing + local newtable = {} + local lasing = 0 + + 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 Tnow = timer.getAbsTime() + -- check recce dead + local recce = entry.lasingunit + if recce and recce:IsAlive() then + valid = valid + 1 + else + reccedead = true + --local text = string.format("Recce %s KIA!",entry.reccename) + --local m = MESSAGE:New(text,15,"Autolase"):ToAll() + 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 + --local text = string.format("Unit %s destroyed! Good job!",entry.unitname) + --local m = MESSAGE:New(text,15,"Autolase"):ToAll() + 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 + local coord = unit:GetCoordinate() -- Core.Point#COORDINATE + local coord2 = recce:GetCoordinate() -- Core.Point#COORDINATE + local dist = coord2:Get2DDistance(coord) + if dist <= self.LaseDistance then + valid = valid + 1 + else + lostsight = true + entry.laserspot:LaseOff() + --local text = string.format("Lost sight of unit %s.",entry.unitname) + --local m = MESSAGE:New(text,15,"Autolase"):ToAll() + self:__TargetLost(2,entry.unitname,entry.reccename) + end + end + -- check timed out + local timestamp = entry.timestamp + if Tnow - timestamp < self.LaseDuration then + valid = valid + 1 + else + lostsight = true + entry.laserspot:LaseOff() + --local text = string.format("Lost sight of unit %s.",entry.unitname) + --local m = MESSAGE:New(text,15,"Autolase"):ToAll() + self:__LaserTimeout(2,entry.unitname,entry.reccename) + end + if valid == 4 then + self.lasingindex = self.lasingindex + 1 + newtable[self.lasingindex] = entry + lasing = lasing + 1 + end + end + self.CurrentLasing = newtable + return lasing +end + +--- (Internal) Function to show status. +-- @param #AUTOLASE self +-- @param Wrapper.Group#GROUP Group (Optional) show to a certain group +-- @return #AUTOLASE self +function AUTOLASE:ShowStatus(Group) + local report = REPORT:New("Autolase") + local lines = 0 + for _ind,_entry in pairs(self.CurrentLasing) do + local entry = _entry -- #AUTOLASE.LaserSpot + local reccename = entry.reccename + local typename = entry.unittype + local code = entry.lasercode + local locationstring = entry.location + local text = string.format("%s lasing %s code %d\nat %s",reccename,typename,code,locationstring) + report:AddIndent(text,"|") + lines = lines + 1 + end + if lines == 0 then + report:AddIndent("No targets!","|") + end + local reporttime = self.reporttimelong + if lines == 0 then reporttime = self.reporttimeshort end + if 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 + +------------------------------------------------------------------- +-- 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:onafterMonitor(From, Event, To) + self:T({From, Event, To}) + + -- Housekeeping + local countlases = self:CleanCurrentLasing() + + local detecteditems = self.Contacts or {} -- #table of Ops.Intelligence#INTEL.Contact + local groupsbythreat = {} + --self:T("Detected Items:") + --self:T({detecteditems}) + 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 + local reccegrp = UNIT:FindByName(reccename) + local reccecoord = reccegrp:GetCoordinate() + local distance = math.floor(reccecoord:Get2DDistance(coord)) + local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, distance/ 1000, contact.threatlevel) + report:Add(text) + self:T(text) + lines = lines + 1 + -- sort out groups beyond sight + if distance <= self.LaseDistance then + table.insert(groupsbythreat,{contact.group,contact.threatlevel}) + self.RecceNames[contact.groupname] = contact.recce + 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) + + --self:T("Groups by Threat") + --self:T({self.GroupsByThreat}) + + -- build table of Units + local unitsbythreat = {} + for _,_entry in ipairs(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() + if threat > 0 then + local unitname = unit:GetName() + 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 ipairs(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) + end + + if self.verbose > 2 and lines > 0 then + local m=MESSAGE:New(unitreport:Text(),self.reporttimeshort,"Autolase"):ToAll() + end + + -- lase targets + local targets = countlases or 0 + for _,_entry in pairs(self.UnitsByThreat) do + local unit = _entry[1] -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + local reccename = self.RecceUnitNames[unitname] + local recce = UNIT:FindByName(reccename) + if targets < self.maxlasing and unit:IsAlive() == true 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() + --local text = string.format("%s is lasing %s code %d\nat %s",reccename,unit:GetTypeName(),code,locationstring) + --local m = MESSAGE:New(text,15,"Autolase"):ToAllIf(self.debug) + local laserspot = { -- #AUTOLASE.LaserSpot + laserspot = spot, + lasedunit = unit, + lasingunit = recce, + lasercode = code, + location = locationstring, + timestamp = timer.getAbsTime(), + unitname = unitname, + reccename = reccename, + unittype = unit:GetTypeName(), + } + if self.smoketargets then + local coord = unit:GetCoordinate() + coord:Smoke(self.smokecolor) + end + self.lasingindex = self.lasingindex + 1 + self.CurrentLasing[self.lasingindex] = laserspot + self:__Lasing(2,laserspot) + end + end + + self:__Monitor(-20) + 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}) + local text = string.format("Recce %s KIA!",RecceName) + local m = MESSAGE:New(text,self.reporttimeshort,"Autolase"):ToAllIf(self.debug) + 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}) + local text = string.format("Unit %s destroyed! Good job!",UnitName) + local m = MESSAGE:New(text,self.reporttimeshort,"Autolase"):ToAllIf(self.debug) + 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}) + local text = string.format("%s lost sight of unit %s.",RecceName,UnitName) + local m = MESSAGE:New(text,self.reporttimeshort,"Autolase"):ToAllIf(self.debug) + 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}) + local text = string.format("%s laser timeout on unit %s.",RecceName,UnitName) + local m = MESSAGE:New(text,self.reporttimeshort,"Autolase"):ToAllIf(self.debug) + 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}) + local laserspot = LaserSpot -- #AUTOLASE.LaserSpot + local text = string.format("%s is lasing %s code %d\nat %s",laserspot.reccename,laserspot.unittype,laserspot.lasercode,laserspot.location) + local m = MESSAGE:New(text,self.reporttimeshort,"Autolase"):ToAllIf(self.debug) + return self +end + +------------------------------------------------------------------- +-- End Functional.Autolase.lua +-------------------------------------------------------------------