diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 23faabb3f..6891fd3fd 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -20,7 +20,7 @@ -- @module Functional.Mantis -- @image Functional.Mantis.jpg --- Date: Apr 2021 +-- Date: July 2021 ------------------------------------------------------------------------- --- **MANTIS** class, extends #Core.Base#BASE @@ -59,7 +59,7 @@ -- @extends Core.Base#BASE ---- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat +--- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat -- -- Simple Class for a more intelligent Air Defense System -- @@ -191,7 +191,20 @@ MANTIS = { ShoradLink = false, ShoradTime = 600, ShoradActDistance = 15000, - UseEmOnOff = true, + UseEmOnOff = false, + TimeStamp = 0, + state2flag = false, + SamStateTracker = {}, + DLink = false, + DLTimeStamp = 0, +} + +--- Advanced state enumerator +-- @type MANTIS.AdvancedState +MANTIS.AdvancedState = { + GREEN = 0, + AMBER = 1, + RED = 2, } ----------------------------------------------------------------------- @@ -208,7 +221,7 @@ do --@param #string coaltion Coalition side of your setup, e.g. "blue", "red" or "neutral" --@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional) --@param #string awacs Group name of your Awacs (optional) - --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional, deault true) + --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN --@return #MANTIS self --@usage Start up your MANTIS with a basic setting -- @@ -263,10 +276,17 @@ do self.ShoradLink = false self.ShoradTime = 600 self.ShoradActDistance = 15000 - -- TODO: add emissions on/off when available .... in 2 weeks + self.TimeStamp = timer.getAbsTime() + self.relointerval = math.random(1800,3600) -- random between 30 and 60 mins + self.state2flag = false + self.SamStateTracker = {} -- table to hold alert states, so we don't trigger state changes twice in adv mode + self.DLink = false + if EmOnOff then if EmOnOff == false then self.UseEmOnOff = false + else + self.UseEmOnOff = true end end @@ -277,7 +297,7 @@ do end -- Inherit everything from BASE class. - local self = BASE:Inherit(self, BASE:New()) -- #MANTIS + local self = BASE:Inherit(self, FSM:New()) -- #MANTIS -- Set the string id for output to DCS.log file. self.lid=string.format("MANTIS %s | ", self.name) @@ -308,27 +328,122 @@ do end -- @field #string version - self.version="0.4.1" + self.version="0.6.2" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) - return self - end + --- FSM Functions --- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- MANTIS status update. + self:AddTransition("*", "Relocating", "*") -- MANTIS HQ and EWR are relocating. + self:AddTransition("*", "GreenState", "*") -- MANTIS A SAM switching to GREEN state. + self:AddTransition("*", "RedState", "*") -- MANTIS A SAM switching to RED state. + self:AddTransition("*", "AdvStateChange", "*") -- MANTIS advanced mode state change. + self:AddTransition("*", "ShoradActivated", "*") -- MANTIS woke up a connected SHORAD. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the MANTIS. Initializes parameters and starts event handlers. + -- @function [parent=#MANTIS] Start + -- @param #MANTIS self + + --- Triggers the FSM event "Start" after a delay. Starts the MANTIS. Initializes parameters and starts event handlers. + -- @function [parent=#MANTIS] __Start + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the MANTIS and all its event handlers. + -- @param #MANTIS self + + --- Triggers the FSM event "Stop" after a delay. Stops the MANTIS and all its event handlers. + -- @function [parent=#MANTIS] __Stop + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#MANTIS] Status + -- @param #MANTIS self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#MANTIS] __Status + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- On After "Relocating" event. HQ and/or EWR moved. + -- @function [parent=#MANTIS] OnAfterRelocating + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + + --- On After "GreenState" event. A SAM group was switched to GREEN alert. + -- @function [parent=#MANTIS] OnAfterGreenState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + + --- On After "RedState" event. A SAM group was switched to RED alert. + -- @function [parent=#MANTIS] OnAfterRedState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + + --- On After "AdvStateChange" event. Advanced state changed, influencing detection speed. + -- @function [parent=#MANTIS] OnAfterAdvStateChange + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red + -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red + -- @param #number Interval Calculated detection interval based on state and advanced feature setting + -- @return #MANTIS self + + --- On After "ShoradActivated" event. Mantis has activated a SHORAD. + -- @function [parent=#MANTIS] OnAfterShoradActivated + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #string Name Name of the GROUP which SHORAD shall protect + -- @param #number Radius Radius around the named group to find SHORAD groups + -- @param #number Ontime Seconds the SHORAD will stay active + + return self + end ----------------------------------------------------------------------- -- MANTIS helper functions ----------------------------------------------------------------------- - --- [internal] Function to get the self.SAM_Table + --- [Internal] Function to get the self.SAM_Table -- @param #MANTIS self -- @return #table table function MANTIS:_GetSAMTable() + self:T(self.lid .. "GetSAMTable") return self.SAM_Table end - --- [internal] Function to set the self.SAM_Table + --- [Internal] Function to set the self.SAM_Table -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_SetSAMTable(table) + self:T(self.lid .. "SetSAMTable") self.SAM_Table = table return self end @@ -337,41 +452,50 @@ do -- @param #MANTIS self -- @param #number radius Radius upon which detected objects will be grouped function MANTIS:SetEWRGrouping(radius) + self:T(self.lid .. "SetEWRGrouping") local radius = radius or 5000 self.grouping = radius + return self end --- Function to set the detection radius of the EWR in meters -- @param #MANTIS self -- @param #number radius Radius of the EWR detection zone function MANTIS:SetEWRRange(radius) + self:T(self.lid .. "SetEWRRange") local radius = radius or 80000 self.acceptrange = radius + return self end --- Function to set switch-on/off zone for the SAM sites in meters -- @param #MANTIS self -- @param #number radius Radius of the firing zone function MANTIS:SetSAMRadius(radius) + self:T(self.lid .. "SetSAMRadius") local radius = radius or 25000 self.checkradius = radius + return self end --- Function to set SAM firing engage range, 0-100 percent, e.g. 75 -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetSAMRange(range) + self:T(self.lid .. "SetSAMRange") local range = range or 75 if range < 0 or range > 100 then range = 75 end self.engagerange = range + return self end --- Function to set a new SAM firing engage range, use this method to adjust range while running MANTIS, e.g. for different setups day and night -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetNewSAMRangeWhileRunning(range) + self:T(self.lid .. "SetNewSAMRangeWhileRunning") local range = range or 75 if range < 0 or range > 100 then range = 75 @@ -379,20 +503,32 @@ do self.engagerange = range self:_RefreshSAMTable() self.mysead.EngagementRange = range + return self end --- Function to set switch-on/off the debug state -- @param #MANTIS self -- @param #boolean onoff Set true to switch on function MANTIS:Debug(onoff) + self:T(self.lid .. "SetDebug") local onoff = onoff or false self.debug = onoff + if onoff then + -- Debug trace. + BASE:TraceOn() + BASE:TraceClass("MANTIS") + BASE:TraceLevel(1) + else + BASE:TraceOff() + end + return self end --- Function to get the HQ object for further use -- @param #MANTIS self -- @return Wrapper.GROUP#GROUP The HQ #GROUP object or *nil* if it doesn't exist function MANTIS:GetCommandCenter() + self:T(self.lid .. "GetCommandCenter") if self.HQ_CC then return self.HQ_CC else @@ -404,26 +540,31 @@ do -- @param #MANTIS self -- @param #string prefix Name of the AWACS group in the mission editor function MANTIS:SetAwacs(prefix) + self:T(self.lid .. "SetAwacs") if prefix ~= nil then if type(prefix) == "string" then self.AWACS_Prefix = prefix self.advAwacs = true end end + return self end --- Function to set AWACS detection range. Defaults to 250.000m (250km) - use **before** starting your Mantis! -- @param #MANTIS self -- @param #number range Detection range of the AWACS group function MANTIS:SetAwacsRange(range) - local range = range or 250000 - self.awacsrange = range + self:T(self.lid .. "SetAwacsRange") + local range = range or 250000 + self.awacsrange = range + return self end --- Function to set the HQ object for further use -- @param #MANTIS self -- @param Wrapper.GROUP#GROUP group The #GROUP object to be set as HQ function MANTIS:SetCommandCenter(group) + self:T(self.lid .. "SetCommandCenter") local group = group or nil if group ~= nil then if type(group) == "string" then @@ -434,14 +575,17 @@ do self.HQ_Template_CC = group:GetName() end end + return self end --- Function to set the detection interval -- @param #MANTIS self -- @param #number interval The interval in seconds function MANTIS:SetDetectInterval(interval) + self:T(self.lid .. "SetDetectInterval") local interval = interval or 30 self.detectinterval = interval + return self end --- Function to set Advanded Mode @@ -451,7 +595,8 @@ do -- @usage Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. -- E.g. `mymantis:SetAdvancedMode(true, 90)` function MANTIS:SetAdvancedMode(onoff, ratio) - self:F({onoff, ratio}) + self:T(self.lid .. "SetAdvancedMode") + --self.T({onoff, ratio}) local onoff = onoff or false local ratio = ratio or 100 if (type(self.HQ_Template_CC) == "string") and onoff and self.dynamic then @@ -459,53 +604,69 @@ do self.advanced = true self.adv_state = 0 self.Adv_EWR_Group = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() - env.info(string.format("***** Starting Advanced Mode MANTIS Version %s *****", self.version)) + self:I(string.format("***** Starting Advanced Mode MANTIS Version %s *****", self.version)) else local text = self.lid.." Advanced Mode requires a HQ and dynamic to be set. Revisit your MANTIS:New() statement to add both." local m= MESSAGE:New(text,10,"MANTIS",true):ToAll() - BASE:E(text) + self:E(text) end + return self end --- Set using Emissions on/off instead of changing alarm state -- @param #MANTIS self -- @param #boolean switch Decide if we are changing alarm state or Emission state function MANTIS:SetUsingEmOnOff(switch) + self:T(self.lid .. "SetUsingEmOnOff") self.UseEmOnOff = switch or false + return self + end + + --- Set using an #INTEL_DLINK object instead of #DETECTION + -- @param #MANTIS self + -- @param Ops.Intelligence#INTEL_DLINK DLink The data link object to be used. + function MANTIS:SetUsingDLink(DLink) + self:T(self.lid .. "SetUsingDLink") + self.DLink = true + self.Detection = DLink + self.DLTimeStamp = timer.getAbsTime() + return self end --- [Internal] Function to check if HQ is alive -- @param #MANTIS self -- @return #boolean True if HQ is alive, else false function MANTIS:_CheckHQState() + self:T(self.lid .. "CheckHQState") local text = self.lid.." Checking HQ State" - self:T(text) local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end -- start check if self.advanced then local hq = self.HQ_Template_CC local hqgrp = GROUP:FindByName(hq) if hqgrp then if hqgrp:IsAlive() then -- ok we're on, hq exists and as alive - env.info(self.lid.." HQ is alive!") + --self.T(self.lid.." HQ is alive!") return true else - env.info(self.lid.." HQ is dead!") + --self.T(self.lid.." HQ is dead!") return false end end - end + end + return self end --- [Internal] Function to check if EWR is (at least partially) alive -- @param #MANTIS self -- @return #boolean True if EWR is alive, else false function MANTIS:_CheckEWRState() + self:T(self.lid .. "CheckEWRState") local text = self.lid.." Checking EWR State" - self:F(text) + --self.T(text) local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end -- start check if self.advanced then local EWR_Group = self.Adv_EWR_Group @@ -519,24 +680,24 @@ do end end end - env.info(self.lid..string.format(" No of EWR alive is %d", nalive)) + --self.T(self.lid..string.format(" No of EWR alive is %d", nalive)) if nalive > 0 then return true else return false end - end + end + return self end --- [Internal] Function to determine state of the advanced mode -- @param #MANTIS self -- @return #number Newly calculated interval -- @return #number Previous state for tracking 0, 1, or 2 - function MANTIS:_CheckAdvState() - local text = self.lid.." Checking Advanced State" - self:F(text) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + function MANTIS:_CalcAdvState() + self:T(self.lid .. "CalcAdvState") + local m=MESSAGE:New(self.lid.." Calculating Advanced State",10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid.." Calculating Advanced State") end -- start check local currstate = self.adv_state -- save curr state for comparison later local EWR_State = self:_CheckEWRState() @@ -554,10 +715,12 @@ do local ratio = self.adv_ratio / 100 -- e.g. 80/100 = 0.8 ratio = ratio * self.adv_state -- e.g 0.8*2 = 1.6 local newinterval = interval + (interval * ratio) -- e.g. 30+(30*1.6) = 78 - local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) - self:F(text) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.debug or self.verbose then + local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) + --self.T(text) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + end return newinterval, currstate end @@ -566,31 +729,34 @@ do -- @param #boolean hq If true, will relocate HQ object -- @param #boolean ewr If true, will relocate EWR objects function MANTIS:SetAutoRelocate(hq, ewr) - self:F({hq, ewr}) + self:T(self.lid .. "SetAutoRelocate") + --self.T({hq, ewr}) local hqrel = hq or false local ewrel = ewr or false if hqrel or ewrel then self.autorelocate = true self.autorelocateunits = { HQ = hqrel, EWR = ewrel } - self:T({self.autorelocate, self.autorelocateunits}) + --self.T({self.autorelocate, self.autorelocateunits}) end + return self end --- [Internal] Function to execute the relocation -- @param #MANTIS self function MANTIS:_RelocateGroups() - self:T(self.lid.." Relocating Groups") + self:T(self.lid .. "RelocateGroups") local text = self.lid.." Relocating Groups" local m= MESSAGE:New(text,10,"MANTIS",true):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end if self.autorelocate then -- relocate HQ - if self.autorelocateunits.HQ and self.HQ_CC then --only relocate if HQ exists + local HQGroup = self.HQ_CC + if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive() then --only relocate if HQ exists local _hqgrp = self.HQ_CC - self:T(self.lid.." Relocating HQ") + --self.T(self.lid.." Relocating HQ") local text = self.lid.." Relocating HQ" - local m= MESSAGE:New(text,10,"MANTIS"):ToAll() - _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) + --local m= MESSAGE:New(text,10,"MANTIS"):ToAll() + _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) end --relocate EWR -- TODO: maybe dependent on AlarmState? Observed: SA11 SR only relocates if no objects in reach @@ -599,38 +765,41 @@ do local EWR_GRP = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() local EWR_Grps = EWR_GRP.Set --table of objects in SET_GROUP for _,_grp in pairs (EWR_Grps) do - if _grp:IsGround() then - self:T(self.lid.." Relocating EWR ".._grp:GetName()) + if _grp:IsAlive() and _grp:IsGround() then + --self.T(self.lid.." Relocating EWR ".._grp:GetName()) local text = self.lid.." Relocating EWR ".._grp:GetName() local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end _grp:RelocateGroundRandomInRadius(20,500,true,true) end end end end + return self end - --- (Internal) Function to check if any object is in the given SAM zone + --- [Internal] Function to check if any object is in the given SAM zone -- @param #MANTIS self -- @param #table dectset Table of coordinates of detected items - -- @param samcoordinate Core.Point#COORDINATE Coordinate object. + -- @param Core.Point#COORDINATE samcoordinate Coordinate object. -- @return #boolean True if in any zone, else false -- @return #number Distance Target distance in meters or zero when no object is in zone function MANTIS:CheckObjectInZone(dectset, samcoordinate) - self:F(self.lid.."CheckObjectInZone Called") + self:T(self.lid.."CheckObjectInZone") -- check if non of the coordinate is in the given defense zone local radius = self.checkradius local set = dectset for _,_coord in pairs (set) do local coord = _coord -- get current coord to check -- output for cross-check - local dectstring = coord:ToStringLLDMS() - local samstring = samcoordinate:ToStringLLDMS() local targetdistance = samcoordinate:DistanceFromPointVec2(coord) - local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) - local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) - if self.verbose then env.info(self.lid..text) end + if self.verbose or self.debug then + local dectstring = coord:ToStringLLDMS() + local samstring = samcoordinate:ToStringLLDMS() + local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) + local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) + self:I(self.lid..text) + end -- end output to cross-check if targetdistance <= radius then return true, targetdistance @@ -639,11 +808,11 @@ do return false, 0 end - --- (Internal) Function to start the detection via EWR groups + --- [Internal] Function to start the detection via EWR groups -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartDetection() - self:F(self.lid.."Starting Detection") + self:T(self.lid.."Starting Detection") -- start detection local groupset = self.EWR_Group @@ -651,14 +820,14 @@ do local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 60 - --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [internal] The MANTIS detection object - _MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[internal] Grouping detected objects to 5000m zones - _MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) - _MANTISdetection:SetAcceptRange(acceptrange) - _MANTISdetection:SetRefreshTimeInterval(interval) - _MANTISdetection:Start() + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object + local MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones + MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) + MANTISdetection:SetAcceptRange(acceptrange) + MANTISdetection:SetRefreshTimeInterval(interval) + MANTISdetection:Start() - function _MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) + function MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) --BASE:I( { From, Event, To, DetectedItem }) local debug = false if DetectedItem.IsDetected and debug then @@ -667,14 +836,14 @@ do local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) end end - return _MANTISdetection + return MANTISdetection end - --- (Internal) Function to start the detection via AWACS if defined as separate + --- [Internal] Function to start the detection via AWACS if defined as separate -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartAwacsDetection() - self:F(self.lid.."Starting Awacs Detection") + self:T(self.lid.."Starting Awacs Detection") -- start detection local group = self.AWACS_Prefix @@ -683,14 +852,14 @@ do --local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 60 - --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [internal] The MANTIS detection object - _MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[internal] Grouping detected objects to 5000m zones - _MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) - _MANTISAwacs:SetAcceptRange(self.awacsrange) --250km - _MANTISAwacs:SetRefreshTimeInterval(interval) - _MANTISAwacs:Start() + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object + local MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones + MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) + MANTISAwacs:SetAcceptRange(self.awacsrange) --250km + MANTISAwacs:SetRefreshTimeInterval(interval) + MANTISAwacs:Start() - function _MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) + function MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) --BASE:I( { From, Event, To, DetectedItem }) local debug = false if DetectedItem.IsDetected and debug then @@ -699,15 +868,15 @@ do local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) end end - return _MANTISAwacs + return MANTISAwacs end - --- (Internal) Function to set the SAM start state + --- [Internal] Function to set the SAM start state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:SetSAMStartState() -- DONE: if using dynamic filtering, update SAM_Table and the (active) SEAD groups, pull req #1405/#1406 - self:F(self.lid.."Setting SAM Start States") + self:T(self.lid.."Setting SAM Start States") -- get SAM Group local SAM_SET = self.SAM_Group local SAM_Grps = SAM_SET.Set --table of objects @@ -716,7 +885,7 @@ do local engagerange = self.engagerange -- firing range in % of max --cycle through groups and set alarm state etc for _i,_group in pairs (SAM_Grps) do - local group = _group + local group = _group -- Wrapper.Group#GROUP -- TODO: add emissions on/off if self.UseEmOnOff then group:EnableEmission(false) @@ -725,11 +894,12 @@ do group:OptionAlarmStateGreen() -- AI off end group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --default engagement will be 75% of firing range - if group:IsGround() then + if group:IsGround() and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() table.insert( SAM_Tbl, {grpname, grpcoord}) table.insert( SEAD_Grps, grpname ) + self.SamStateTracker[grpname] = "GREEN" end end self.SAM_Table = SAM_Tbl @@ -740,11 +910,11 @@ do return self end - --- (Internal) Function to update SAM table and SEAD state + --- [Internal] Function to update SAM table and SEAD state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_RefreshSAMTable() - self:F(self.lid.."Setting SAM Start States") + self:T(self.lid.."RefreshSAMTable") -- Requires SEAD 0.2.2 or better -- get SAM Group local SAM_SET = self.SAM_Group @@ -756,7 +926,7 @@ do for _i,_group in pairs (SAM_Grps) do local group = _group group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --engagement will be 75% of firing range - if group:IsGround() then + if group:IsGround() and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() table.insert( SAM_Tbl, {grpname, grpcoord}) -- make the table lighter, as I don't really use the zone here @@ -777,6 +947,7 @@ do -- @param Functional.Shorad#SHORAD Shorad The #SHORAD object -- @param #number Shoradtime Number of seconds #SHORAD stays active post wake-up function MANTIS:AddShorad(Shorad,Shoradtime) + self:T(self.lid.."AddShorad") local Shorad = Shorad or nil local ShoradTime = Shoradtime or 600 local ShoradLink = true @@ -785,184 +956,311 @@ do self.Shorad = Shorad --#SHORAD self.ShoradTime = Shoradtime -- #number end + return self end --- Function to unlink #MANTIS from a #SHORAD installation -- @param #MANTIS self function MANTIS:RemoveShorad() + self:T(self.lid.."RemoveShorad") self.ShoradLink = false + return self end ----------------------------------------------------------------------- -- MANTIS main functions ----------------------------------------------------------------------- - --- Function to set the SAM start state + --- [Internal] Check detection function + -- @param #MANTIS self + -- @param Functional.Detection#DETECTION_AREAS detection Detection object + -- @return #MANTIS self + function MANTIS:_Check(detection) + self:T(self.lid .. "Check") + --get detected set + local detset = detection:GetDetectedItemCoordinates() + self:T("Check:", {detset}) + -- randomly update SAM Table + local rand = math.random(1,100) + if rand > 65 then -- 1/3 of cases + self:_RefreshSAMTable() + end + -- switch SAMs on/off if (n)one of the detected groups is inside their reach + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates + for _,_data in pairs (samset) do + local samcoordinate = _data[2] + local name = _data[1] + local samgroup = GROUP:FindByName(name) + local IsInZone, Distance = self:CheckObjectInZone(detset, samcoordinate) + if IsInZone then --check any target in zone + if samgroup:IsAlive() then + -- switch on SAM + if self.UseEmOnOff then + -- TODO: add emissions on/off + --samgroup:SetAIOn() + samgroup:EnableEmission(true) + end + samgroup:OptionAlarmStateRed() + if self.SamStateTracker[name] ~= "RED" then + self:__RedState(1,samgroup) + self.SamStateTracker[name] = "RED" + end + -- link in to SHORAD if available + -- DONE: Test integration fully + if self.ShoradLink and Distance < self.ShoradActDistance then -- don't give SHORAD position away too early + local Shorad = self.Shorad + local radius = self.checkradius + local ontime = self.ShoradTime + Shorad:WakeUpShorad(name, radius, ontime) + self:__ShoradActivated(1,name, radius, ontime) + end + -- debug output + if self.debug or self.verbose then + local text = string.format("SAM %s switched to alarm state RED!", name) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid..text) end + end + end --end alive + else + if samgroup:IsAlive() then + -- switch off SAM + if self.UseEmOnOff then + samgroup:EnableEmission(false) + end + samgroup:OptionAlarmStateGreen() + if self.SamStateTracker[name] ~= "GREEN" then + self:__GreenState(1,samgroup) + self.SamStateTracker[name] = "GREEN" + end + if self.debug or self.verbose then + local text = string.format("SAM %s switched to alarm state GREEN!", name) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid..text) end + end + end --end alive + end --end check + end --for for loop + return self + end + + --- [Internal] Relocation relay function -- @param #MANTIS self -- @return #MANTIS self - function MANTIS:Start() - self:F(self.lid.."Starting MANTIS") - self:SetSAMStartState() - self.Detection = self:StartDetection() - if self.advAwacs then - self.AWACS_Detection = self:StartAwacsDetection() - end - -- detection function - local function check(detection) - --get detected set - local detset = detection:GetDetectedItemCoordinates() - self:F("Check:", {detset}) - -- randomly update SAM Table - local rand = math.random(1,100) - if rand > 65 then -- 1/3 of cases - self:_RefreshSAMTable() - end - -- switch SAMs on/off if (n)one of the detected groups is inside their reach - local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates - for _,_data in pairs (samset) do - local samcoordinate = _data[2] - local name = _data[1] - local samgroup = GROUP:FindByName(name) - local IsInZone, Distance = self:CheckObjectInZone(detset, samcoordinate) - if IsInZone then --check any target in zone + function MANTIS:_Relocate() + self:T(self.lid .. "Relocate") + self:_RelocateGroups() + return self + end + + --- [Internal] Check advanced state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_CheckAdvState() + self:T(self.lid .. "CheckAdvSate") + local interval, oldstate = self:_CalcAdvState() + local newstate = self.adv_state + if newstate ~= oldstate then + -- deal with new state + self:__AdvStateChange(1,oldstate,newstate,interval) + if newstate == 2 then + -- switch alarm state RED + self.state2flag = true + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates + for _,_data in pairs (samset) do + local name = _data[1] + local samgroup = GROUP:FindByName(name) if samgroup:IsAlive() then - -- switch on SAM if self.UseEmOnOff then -- TODO: add emissions on/off --samgroup:SetAIOn() samgroup:EnableEmission(true) end samgroup:OptionAlarmStateRed() - -- link in to SHORAD if available - -- DONE: Test integration fully - if self.ShoradLink and Distance < self.ShoradActDistance then -- don't give SHORAD position away too early - local Shorad = self.Shorad - local radius = self.checkradius - local ontime = self.ShoradTime - Shorad:WakeUpShorad(name, radius, ontime) - end - -- debug output - local text = string.format("SAM %s switched to alarm state RED!", name) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(self.lid..text) end - end --end alive - else - if samgroup:IsAlive() then - -- switch off SAM - if self.UseEmOnOff then - -- TODO: add emissions on/off - samgroup:EnableEmission(false) - --samgroup:SetAIOff() - else - samgroup:OptionAlarmStateGreen() - end - --samgroup:OptionROEWeaponFree() - --samgroup:SetAIOn() - local text = string.format("SAM %s switched to alarm state GREEN!", name) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(self.lid..text) end - end --end alive - end --end check - end --for for loop - end --end function - -- relocation relay function - local function relocate() - self:_RelocateGroups() - end - -- check advanced state - local function checkadvstate() - local interval, oldstate = self:_CheckAdvState() - local newstate = self.adv_state - if newstate ~= oldstate then - -- deal with new state - if newstate == 2 then - -- switch alarm state RED - if self.MantisTimer.isrunning then - self.MantisTimer:Stop() - self.MantisTimer.isrunning = false - end -- stop Awacs timer - if self.MantisATimer.isrunning then - self.MantisATimer:Stop() - self.MantisATimer.isrunning = false - end -- stop timer - local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates - for _,_data in pairs (samset) do - local name = _data[1] - local samgroup = GROUP:FindByName(name) - if samgroup:IsAlive() then - if self.UseEmOnOff then - -- TODO: add emissions on/off - --samgroup:SetAIOn() - samgroup:EnableEmission(true) - end - samgroup:OptionAlarmStateRed() - end -- end alive - end -- end for loop - elseif newstate <= 1 then - -- change MantisTimer to slow down or speed up - if self.MantisTimer.isrunning then - self.MantisTimer:Stop() - self.MantisTimer.isrunning = false - end - if self.MantisATimer.isrunning then - self.MantisATimer:Stop() - self.MantisATimer.isrunning = false - end - self.MantisTimer = TIMER:New(check,self.Detection) - self.MantisTimer:Start(5,interval,nil) - self.MantisTimer.isrunning = true - if self.advAwacs then - self.MantisATimer = TIMER:New(check,self.AWACS_Detection) - self.MantisATimer:Start(15,interval,nil) - self.MantisATimer.isrunning = true - end - end - end -- end newstate vs oldstate - end - -- timers to run the system - local interval = self.detectinterval - self.MantisTimer = TIMER:New(check,self.Detection) - self.MantisTimer:Start(5,interval,nil) - self.MantisTimer.isrunning = true - -- Awacs timer - if self.advAwacs then - self.MantisATimer = TIMER:New(check,self.AWACS_Detection) - self.MantisATimer:Start(15,interval,nil) - self.MantisATimer.isrunning = true - end - -- timer to relocate HQ and EWR - if self.autorelocate then - local relointerval = math.random(1800,3600) -- random between 30 and 60 mins - self.MantisReloTimer = TIMER:New(relocate) - self.MantisReloTimer:Start(relointerval,relointerval,nil) - end - -- timer for advanced state check - if self.advanced then - self.MantisAdvTimer = TIMER:New(checkadvstate) - self.MantisAdvTimer:Start(30,interval*5,nil) - end + end -- end alive + end -- end for loop + elseif newstate <= 1 then + -- change MantisTimer to slow down or speed up + self.detectinterval = interval + self.state2flag = false + end + end -- end newstate vs oldstate return self end - --- Function to stop MANTIS + --- [Internal] Check DLink state -- @param #MANTIS self -- @return #MANTIS self - function MANTIS:Stop() - if self.MantisTimer.isrunning then - self.MantisTimer:Stop() + function MANTIS:_CheckDLinkState() + self:T(self.lid .. "_CheckDLinkState") + local dlink = self.Detection -- Ops.Intelligence#INTEL_DLINK + local TS = timer.getAbsTime() + if not dlink:Is("Running") and (TS - self.DLTimeStamp > 29) then + self.DLink = false + self.Detection = self:StartDetection() -- fall back + self:I(self.lid .. "Intel DLink not running - switching back to single detection!") end - if self.MantisATimer.isrunning then - self.MantisATimer:Stop() + end + + --- [Internal] Function to set start state + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."Starting MANTIS") + self:SetSAMStartState() + if not self.DLink then + self.Detection = self:StartDetection() end - if self.autorelocate then - self.MantisReloTimer:Stop() + if self.advAwacs then + self.AWACS_Detection = self:StartAwacsDetection() end - if self.advanced then - self.MantisAdvTimer:Stop() - end - return self + self:__Status(-math.random(1,10)) + return self end + --- [Internal] Before status function for MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + -- check detection + if not self.state2flag then + self:_Check(self.Detection) + end + + -- check Awacs + if self.advAwacs and not self.state2flag then + self:_Check(self.AWACS_Detection) + end + + -- relocate HQ and EWR + if self.autorelocate then + local relointerval = self.relointerval + local thistime = timer.getAbsTime() + local timepassed = thistime - self.TimeStamp + + local halfintv = math.floor(timepassed / relointerval) + + --self:T({timepassed=timepassed, halfintv=halfintv}) + + if halfintv >= 1 then + self.TimeStamp = timer.getAbsTime() + self:_Relocate() + self:__Relocating(1) + end + end + + -- advanced state check + if self.advanced then + self:_CheckAdvState() + end + + -- check DLink state + if self.DLink then + self:_CheckDLinkState() + end + + return self + end + + --- [Internal] Status function for MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStatus(From,Event,To) + self:T({From, Event, To}) + -- Display some states + if self.debug then + self:I(self.lid .. "Status Report") + for _name,_state in pairs(self.SamStateTracker) do + self:I(string.format("Site %s\tStatus %s",_name,_state)) + end + end + local interval = self.detectinterval * -1 + self:__Status(interval) + return self + end + + --- [Internal] Function to stop MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStop(From, Event, To) + self:T({From, Event, To}) + return self + end + + --- [Internal] Function triggered by Event Relocating + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterRelocating(From, Event, To) + self:T({From, Event, To}) + return self + end + + --- [Internal] Function triggered by Event GreenState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + function MANTIS:onafterGreenState(From, Event, To, Group) + self:T({From, Event, To, Group}) + return self + end + + --- [Internal] Function triggered by Event RedState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + function MANTIS:onafterRedState(From, Event, To, Group) + self:T({From, Event, To, Group}) + return self + end + + --- [Internal] Function triggered by Event AdvStateChange + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red + -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red + -- @param #number Interval Calculated detection interval based on state and advanced feature setting + -- @return #MANTIS self + function MANTIS:onafterAdvStateChange(From, Event, To, Oldstate, Newstate, Interval) + self:T({From, Event, To, Oldstate, Newstate, Interval}) + return self + end + + --- [Internal] Function triggered by Event ShoradActivated + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #string Name Name of the GROUP which SHORAD shall protect + -- @param #number Radius Radius around the named group to find SHORAD groups + -- @param #number Ontime Seconds the SHORAD will stay active + function MANTIS:onafterShoradActivated(From, Event, To, Name, Radius, Ontime) + self:T({From, Event, To, Name, Radius, Ontime}) + return self + end end ----------------------------------------------------------------------- -- MANTIS end diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 8eb98d4f4..c99e1f241 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -1,26 +1,26 @@ --- **Functional** -- Make SAM sites execute evasive and defensive behaviour when being fired upon. --- +-- -- === --- +-- -- ## Features: --- +-- -- * When SAM sites are being fired upon, the SAMs will take evasive action will reposition themselves when possible. -- * When SAM sites are being fired upon, the SAMs will take defensive action by shutting down their radars. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [SEV - SEAD Evasion](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SEV%20-%20SEAD%20Evasion) --- +-- -- === --- +-- -- ### Authors: **FlightControl**, **applevangelist** --- --- Last Update: April 2021 --- +-- +-- Last Update: July 2021 +-- -- === --- +-- -- @module Functional.Sead -- @image SEAD.JPG @@ -28,49 +28,32 @@ -- @extends Core.Base#BASE --- Make SAM sites execute evasive and defensive behaviour when being fired upon. --- +-- -- This class is very easy to use. Just setup a SEAD object by using @{#SEAD.New}() and SAMs will evade and take defensive action when being fired upon. --- +-- -- # Constructor: --- +-- -- Use the @{#SEAD.New}() constructor to create a new SEAD object. --- +-- -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) --- +-- -- @field #SEAD SEAD = { - ClassName = "SEAD", - TargetSkill = { - Average = { Evade = 30, DelayOn = { 40, 60 } } , - Good = { Evade = 20, DelayOn = { 30, 50 } } , - High = { Evade = 15, DelayOn = { 20, 40 } } , - Excellent = { Evade = 10, DelayOn = { 10, 30 } } - }, - SEADGroupPrefixes = {}, - SuppressedGroups = {}, - EngagementRange = 75 -- default 75% engagement range Feature Request #1355 + ClassName = "SEAD", + TargetSkill = { + Average = { Evade = 30, DelayOn = { 40, 60 } } , + Good = { Evade = 20, DelayOn = { 30, 50 } } , + High = { Evade = 15, DelayOn = { 20, 40 } } , + Excellent = { Evade = 10, DelayOn = { 10, 30 } } + }, + SEADGroupPrefixes = {}, + SuppressedGroups = {}, + EngagementRange = 75 -- default 75% engagement range Feature Request #1355 } - -- TODO Complete list? --- Missile enumerators -- @field Harms SEAD.Harms = { - --[[ - ["X58"] = "weapons.missiles.X_58", --Kh-58X anti-radiation missiles fired - ["Kh25"] = "weapons.missiles.Kh25MP_PRGS1VP", --Kh-25MP anti-radiation missiles fired - ["X25"] = "weapons.missiles.X_25MP", --Kh-25MPU anti-radiation missiles fired - ["X28"] = "weapons.missiles.X_28", --Kh-28 anti-radiation missiles fired - ["X31"] = "weapons.missiles.X_31P", --Kh-31P anti-radiation missiles fired - ["AGM45A"] = "weapons.missiles.AGM_45A", --AGM-45A anti-radiation missiles fired - ["AGM45"] = "weapons.missiles.AGM_45", --AGM-45B anti-radiation missiles fired - ["AGM88"] = "weapons.missiles.AGM_88", --AGM-88C anti-radiation missiles fired - ["AGM122"] = "weapons.missiles.AGM_122", --AGM-122 Sidearm anti-radiation missiles fired - ["LD10"] = "weapons.missiles.LD-10", --LD-10 anti-radiation missiles fired - ["ALARM"] = "weapons.missiles.ALARM", --ALARM anti-radiation missiles fired - ["AGM84E"] = "weapons.missiles.AGM_84E", --AGM84 anti-radiation missiles fired - ["AGM84A"] = "weapons.missiles.AGM_84A", --AGM84 anti-radiation missiles fired - ["AGM84H"] = "weapons.missiles.AGM_84H", --AGM84 anti-radiation missiles fired - --]] ["AGM_88"] = "AGM_88", ["AGM_45"] = "AGM_45", ["AGM_122"] = "AGM_122", @@ -84,7 +67,7 @@ SEAD = { ["X_31"] = "X_31", ["Kh25"] = "Kh25", } - + --- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. -- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... -- Chances are big that the missile will miss. @@ -97,20 +80,20 @@ SEAD = { -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) function SEAD:New( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - - if type( SEADGroupPrefixes ) == 'table' then - for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do - self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix - end - else - self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes - end - - self:HandleEvent( EVENTS.Shot ) - self:I("*** SEAD - Started Version 0.2.7") - return self + local self = BASE:Inherit( self, BASE:New() ) + self:F( SEADGroupPrefixes ) + + if type( SEADGroupPrefixes ) == 'table' then + for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do + self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix + end + else + self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes + end + + self:HandleEvent( EVENTS.Shot, self.HandleEventShot ) + self:I("*** SEAD - Started Version 0.2.8") + return self end --- Update the active SEAD Set @@ -120,7 +103,7 @@ end function SEAD:UpdateSet( SEADGroupPrefixes ) self:F( SEADGroupPrefixes ) - + if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -164,112 +147,83 @@ end -- @see SEAD -- @param #SEAD -- @param Core.Event#EVENTDATA EventData -function SEAD:OnEventShot( EventData ) - self:T( { EventData } ) +function SEAD:HandleEventShot( EventData ) + self:T( { EventData } ) - local SEADUnit = EventData.IniDCSUnit - local SEADUnitName = EventData.IniDCSUnitName - local SEADWeapon = EventData.Weapon -- Identify the weapon fired - local SEADWeaponName = EventData.WeaponName -- return weapon type + local SEADUnit = EventData.IniDCSUnit + local SEADUnitName = EventData.IniDCSUnitName + local SEADWeapon = EventData.Weapon -- Identify the weapon fired + local SEADWeaponName = EventData.WeaponName -- return weapon type - self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName) - self:T({ SEADWeapon }) - - --[[check for SEAD missiles - if SEADWeaponName == "weapons.missiles.X_58" --Kh-58U anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.Kh25MP_PRGS1VP" --Kh-25MP anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.X_25MP" --Kh-25MPU anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.X_28" --Kh-28 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.X_31P" --Kh-31P anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_45A" --AGM-45A anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_45" --AGM-45B anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_88" --AGM-88C anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_122" --AGM-122 Sidearm anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.LD-10" --LD-10 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.ALARM" --ALARM anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_84E" --AGM84 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_84A" --AGM84 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_84H" --AGM84 anti-radiation missiles fired - --]] + self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName) + self:T({ SEADWeapon }) + if self:_CheckHarms(SEADWeaponName) then local _targetskill = "Random" local _targetMimgroupName = "none" - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = EventData.Weapon:getTarget() -- Identify target - local _targetUnit = UNIT:Find(_targetMim) -- Unit name by DCS Object - if _targetUnit and _targetUnit:IsAlive() then - local _targetMimgroup = _targetUnit:GetGroup() - local _targetMimgroupName = _targetMimgroup:GetName() -- group name - --local _targetskill = _DATABASE.Templates.Units[_targetUnit].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) - end - -- see if we are shot at - local SEADGroupFound = false - for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then - SEADGroupFound = true - self:T( '*** SEAD - Group Found' ) - break - end - end - if SEADGroupFound == true then -- yes we are being attacked - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - self:T( _targetskill ) - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then + local _evade = math.random (1,100) -- random number for chance of evading action + local _targetMim = EventData.Weapon:getTarget() -- Identify target + local _targetUnit = UNIT:Find(_targetMim) -- Unit name by DCS Object + if _targetUnit and _targetUnit:IsAlive() then + local _targetMimgroup = _targetUnit:GetGroup() + local _targetMimgroupName = _targetMimgroup:GetName() -- group name + --local _targetskill = _DATABASE.Templates.Units[_targetUnit].Template.skill + self:T( self.SEADGroupPrefixes ) + self:T( _targetMimgroupName ) + end + -- see if we are shot at + local SEADGroupFound = false + for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do + if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then + SEADGroupFound = true + self:T( '*** SEAD - Group Found' ) + break + end + end + if SEADGroupFound == true then -- yes we are being attacked + if _targetskill == "Random" then -- when skill is random, choose a skill + local Skills = { "Average", "Good", "High", "Excellent" } + _targetskill = Skills[ math.random(1,4) ] + end + self:T( _targetskill ) + if self.TargetSkill[_targetskill] then + if (_evade > self.TargetSkill[_targetskill].Evade) then + + self:T( string.format("*** SEAD - Evading, target skill " ..string.format(_targetskill)) ) + + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimcont= _targetMimgroup:getController() + + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + + --tracker ID table to switch groups off and on again + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } - self:T( string.format("*** SEAD - Evading, target skill " ..string.format(_targetskill)) ) - - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - - --tracker ID table to switch groups off and on again - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - - local function SuppressionEnd(id) --switch group back on - local range = self.EngagementRange -- Feature Request #1355 - self:T(string.format("*** SEAD - Engagement Range is %d", range)) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - --id.groupName:enableEmission(true) - id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) --Feature Request #1355 - self.SuppressedGroups[id.groupName] = nil --delete group id from table when done - end - -- randomize switch-on time - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - local SuppressionEndTime = timer.getTime() + delay - --create entry - if self.SuppressedGroups[id.groupName] == nil then --no timer entry for this group yet - self.SuppressedGroups[id.groupName] = { - SuppressionEndTime = delay - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - --_targetMimgroup:enableEmission(false) - timer.scheduleFunction(SuppressionEnd, id, SuppressionEndTime) --Schedule the SuppressionEnd() function - end - end - end - end - end + local function SuppressionEnd(id) --switch group back on + local range = self.EngagementRange -- Feature Request #1355 + self:T(string.format("*** SEAD - Engagement Range is %d", range)) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) + --id.groupName:enableEmission(true) + id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) --Feature Request #1355 + self.SuppressedGroups[id.groupName] = nil --delete group id from table when done + end + -- randomize switch-on time + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + local SuppressionEndTime = timer.getTime() + delay + --create entry + if self.SuppressedGroups[id.groupName] == nil then --no timer entry for this group yet + self.SuppressedGroups[id.groupName] = { + SuppressionEndTime = delay + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + --_targetMimgroup:enableEmission(false) + timer.scheduleFunction(SuppressionEnd, id, SuppressionEndTime) --Schedule the SuppressionEnd() function + end + end + end + end + end end diff --git a/Moose Development/Moose/Functional/Shorad.lua b/Moose Development/Moose/Functional/Shorad.lua index 1dbff94f4..958877ec8 100644 --- a/Moose Development/Moose/Functional/Shorad.lua +++ b/Moose Development/Moose/Functional/Shorad.lua @@ -1,24 +1,24 @@ --- **Functional** -- Short Range Air Defense System --- +-- -- === --- +-- -- **SHORAD** - Short Range Air Defense System -- Controls a network of short range air/missile defense groups. --- +-- -- === --- +-- -- ## Missions: -- -- ### [SHORAD - Short Range Air Defense](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SRD%20-%20SHORAD%20Defense) --- +-- -- === --- --- ### Author : **applevangelist** --- +-- +-- ### Author : **applevangelist ** +-- -- @module Functional.Shorad -- @image Functional.Shorad.jpg -- --- Date: May 2021 +-- Date: July 2021 ------------------------------------------------------------------------- --- **SHORAD** class, extends Core.Base#BASE @@ -26,7 +26,7 @@ -- @field #string ClassName -- @field #string name Name of this Shorad -- @field #boolean debug Set the debug state --- @field #string Prefixes String to be used to build the @{#Core.Set#SET_GROUP} +-- @field #string Prefixes String to be used to build the @{#Core.Set#SET_GROUP} -- @field #number Radius Shorad defense radius in meters -- @field Core.Set#SET_GROUP Groupset The set of Shorad groups -- @field Core.Set#SET_GROUP Samset The set of SAM groups to defend @@ -41,10 +41,10 @@ -- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green. -- @extends Core.Base#BASE ---- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie) --- +--- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie) +-- -- Simple Class for a more intelligent Short Range Air Defense System --- +-- -- #SHORAD -- Moose derived missile intercepting short range defense system. -- Protects a network of SAM sites. Uses events to switch on the defense groups closest to the enemy. @@ -52,26 +52,26 @@ -- -- ## Usage -- --- Set up a #SET_GROUP for the SAM sites to be protected: --- --- `local SamSet = SET_GROUP:New():FilterPrefixes("Red SAM"):FilterCoalitions("red"):FilterStart()` --- +-- Set up a #SET_GROUP for the SAM sites to be protected: +-- +-- `local SamSet = SET_GROUP:New():FilterPrefixes("Red SAM"):FilterCoalitions("red"):FilterStart()` +-- -- By default, SHORAD will defense against both HARMs and AG-Missiles with short to medium range. The default defense probability is 70-90%. --- When a missile is detected, SHORAD will activate defense groups in the given radius around the target for 10 minutes. It will *not* react to friendly fire. --- +-- When a missile is detected, SHORAD will activate defense groups in the given radius around the target for 10 minutes. It will *not* react to friendly fire. +-- -- ### Start a new SHORAD system, parameters are: +-- +-- * Name: Name of this SHORAD. +-- * ShoradPrefix: Filter for the Shorad #SET_GROUP. +-- * Samset: The #SET_GROUP of SAM sites to defend. +-- * Radius: Defense radius in meters. +-- * ActiveTimer: Determines how many seconds the systems stay on red alert after wake-up call. +-- * Coalition: Coalition, i.e. "blue", "red", or "neutral".* +-- +-- `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")` -- --- * Name: Name of this SHORAD. --- * ShoradPrefix: Filter for the Shorad #SET_GROUP. --- * Samset: The #SET_GROUP of SAM sites to defend. --- * Radius: Defense radius in meters. --- * ActiveTimer: Determines how many seconds the systems stay on red alert after wake-up call. --- * Coalition: Coalition, i.e. "blue", "red", or "neutral".* --- --- `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")` --- --- ## Customize options --- +-- ## Customize options +-- -- * SHORAD:SwitchDebug(debug) -- * SHORAD:SwitchHARMDefense(onoff) -- * SHORAD:SwitchAGMDefense(onoff) @@ -94,9 +94,9 @@ SHORAD = { lid = "", DefendHarms = true, DefendMavs = true, - DefenseLowProb = 75, + DefenseLowProb = 70, DefenseHighProb = 90, - UseEmOnOff = false, + UseEmOnOff = false, } ----------------------------------------------------------------------- @@ -108,22 +108,6 @@ do --- Missile enumerators -- @field Harms SHORAD.Harms = { - --[[ - ["X58"] = "weapons.missiles.X_58", --Kh-58X anti-radiation missiles fired - ["Kh25"] = "weapons.missiles.Kh25MP_PRGS1VP", --Kh-25MP anti-radiation missiles fired - ["X25"] = "weapons.missiles.X_25MP", --Kh-25MPU anti-radiation missiles fired - ["X28"] = "weapons.missiles.X_28", --Kh-28 anti-radiation missiles fired - ["X31"] = "weapons.missiles.X_31P", --Kh-31P anti-radiation missiles fired - ["AGM45A"] = "weapons.missiles.AGM_45A", --AGM-45A anti-radiation missiles fired - ["AGM45"] = "weapons.missiles.AGM_45", --AGM-45B anti-radiation missiles fired - ["AGM88"] = "weapons.missiles.AGM_88", --AGM-88C anti-radiation missiles fired - ["AGM122"] = "weapons.missiles.AGM_122", --AGM-122 Sidearm anti-radiation missiles fired - ["LD10"] = "weapons.missiles.LD-10", --LD-10 anti-radiation missiles fired - ["ALARM"] = "weapons.missiles.ALARM", --ALARM anti-radiation missiles fired - ["AGM84E"] = "weapons.missiles.AGM_84E", --AGM84 anti-radiation missiles fired - ["AGM84A"] = "weapons.missiles.AGM_84A", --AGM84 anti-radiation missiles fired - ["AGM84H"] = "weapons.missiles.AGM_84H", --AGM84 anti-radiation missiles fired - --]] ["AGM_88"] = "AGM_88", ["AGM_45"] = "AGM_45", ["AGM_122"] = "AGM_122", @@ -137,7 +121,7 @@ do ["X_31"] = "X_31", ["Kh25"] = "Kh25", } - + --- TODO complete list? -- @field Mavs SHORAD.Mavs = { @@ -148,7 +132,7 @@ do ["Kh31"] = "Kh31", ["Kh66"] = "Kh66", } - + --- Instantiates a new SHORAD object -- @param #SHORAD self -- @param #string Name Name of this SHORAD @@ -157,10 +141,12 @@ do -- @param #number Radius Defense radius in meters, used to switch on groups -- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call -- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral" - function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition) + -- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch) + -- @retunr #SHORAD self + function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff) local self = BASE:Inherit( self, BASE:New() ) self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition}) - + local GroupSet = SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart() self.name = Name or "MyShorad" @@ -171,22 +157,23 @@ do self.ActiveTimer = ActiveTimer or 600 self.ActiveGroups = {} self.Groupset = GroupSet - self:HandleEvent( EVENTS.Shot ) self.DefendHarms = true self.DefendMavs = true self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin - self.UseEmOnOff = true -- Decide if we are using Emission on/off (default) or AlarmState red/green - self:I("*** SHORAD - Started Version 0.2.5") + self.UseEmOnOff = UseEmOnOff or false -- Decide if we are using Emission on/off (default) or AlarmState red/green + self:I("*** SHORAD - Started Version 0.2.8") -- Set the string id for output to DCS.log file. self.lid=string.format("SHORAD %s | ", self.name) self:_InitState() + self:HandleEvent(EVENTS.Shot, self.HandleEventShot) return self end - + --- Initially set all groups to alarm state GREEN -- @param #SHORAD self function SHORAD:_InitState() + self:T(self.lid .. " _InitState") local table = {} local set = self.Groupset self:T({set = set}) @@ -195,33 +182,50 @@ do if self.UseEmOnOff then --_group:SetAIOff() _group:EnableEmission(false) + _group:OptionAlarmStateRed() --Wrapper.Group#GROUP else _group:OptionAlarmStateGreen() --Wrapper.Group#GROUP end + _group:OptionDisperseOnAttack(30) end -- gather entropy - for i=1,10 do + for i=1,100 do math.random() end + return self end - - --- Switch debug state + + --- Switch debug state on -- @param #SHORAD self -- @param #boolean debug Switch debug on (true) or off (false) - function SHORAD:SwitchDebug(debug) - self:T( { debug } ) - local onoff = debug or false - if debug then - self.debug = true - --tracing - BASE:TraceOn() - BASE:TraceClass("SHORAD") + function SHORAD:SwitchDebug(onoff) + self:T( { onoff } ) + if onoff then + self:SwitchDebugOn() else - self.debug = false - BASE:TraceOff() + self.SwitchDebugOff() end + return self end - + + --- Switch debug state on + -- @param #SHORAD self + function SHORAD:SwitchDebugOn() + self.debug = true + --tracing + BASE:TraceOn() + BASE:TraceClass("SHORAD") + return self + end + + --- Switch debug state off + -- @param #SHORAD self + function SHORAD:SwitchDebugOff() + self.debug = false + BASE:TraceOff() + return self + end + --- Switch defense for HARMs -- @param #SHORAD self -- @param #boolean onoff @@ -229,8 +233,9 @@ do self:T( { onoff } ) local onoff = onoff or true self.DefendHarms = onoff + return self end - + --- Switch defense for AGMs -- @param #SHORAD self -- @param #boolean onoff @@ -238,8 +243,9 @@ do self:T( { onoff } ) local onoff = onoff or true self.DefendMavs = onoff + return self end - + --- Set defense probability limits -- @param #SHORAD self -- @param #number low Minimum detection limit, integer 1-100 @@ -256,42 +262,50 @@ do end self.DefenseLowProb = low self.DefenseHighProb = high + return self end - + --- Set the number of seconds a SHORAD site will stay active -- @param #SHORAD self -- @param #number seconds Number of seconds systems stay active function SHORAD:SetActiveTimer(seconds) + self:T(self.lid .. " SetActiveTimer") local timer = seconds or 600 if timer < 0 then timer = 600 end self.ActiveTimer = timer + return self end --- Set the number of meters for the SHORAD defense zone -- @param #SHORAD self - -- @param #number meters Radius of the defense search zone in meters. #SHORADs in this range around a targeted group will go active + -- @param #number meters Radius of the defense search zone in meters. #SHORADs in this range around a targeted group will go active function SHORAD:SetDefenseRadius(meters) + self:T(self.lid .. " SetDefenseRadius") local radius = meters or 20000 if radius < 0 then radius = 20000 end self.Radius = radius + return self end - + --- Set using Emission on/off instead of changing alarm state -- @param #SHORAD self -- @param #boolean switch Decide if we are changing alarm state or AI state function SHORAD:SetUsingEmOnOff(switch) + self:T(self.lid .. " SetUsingEmOnOff") self.UseEmOnOff = switch or false + return self end - + --- Check if a HARM was fired -- @param #SHORAD self -- @param #string WeaponName -- @return #boolean Returns true for a match function SHORAD:_CheckHarms(WeaponName) + self:T(self.lid .. " _CheckHarms") self:T( { WeaponName } ) local hit = false if self.DefendHarms then @@ -301,12 +315,13 @@ do end return hit end - + --- Check if an AGM was fired -- @param #SHORAD self -- @param #string WeaponName -- @return #boolean Returns true for a match function SHORAD:_CheckMavs(WeaponName) + self:T(self.lid .. " _CheckMavs") self:T( { WeaponName } ) local hit = false if self.DefendMavs then @@ -316,15 +331,16 @@ do end return hit end - + --- Check the coalition of the attacker -- @param #SHORAD self -- @param #string Coalition name -- @return #boolean Returns false for a match function SHORAD:_CheckCoalition(Coalition) + self:T(self.lid .. " _CheckCoalition") local owncoalition = self.Coalition local othercoalition = "" - if Coalition == 0 then + if Coalition == 0 then othercoalition = "neutral" elseif Coalition == 1 then othercoalition = "red" @@ -338,12 +354,13 @@ do return false end end - + --- Check if the missile is aimed at a SHORAD -- @param #SHORAD self -- @param #string TargetGroupName Name of the target group -- @return #boolean Returns true for a match, else false function SHORAD:_CheckShotAtShorad(TargetGroupName) + self:T(self.lid .. " _CheckShotAtShorad") local tgtgrp = TargetGroupName local shorad = self.Groupset local shoradset = shorad:GetAliveSet() --#table @@ -352,17 +369,18 @@ do local groupname = _groups:GetName() if string.find(groupname, tgtgrp, 1) then returnname = true - _groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive + --_groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive end end - return returnname + return returnname end - + --- Check if the missile is aimed at a SAM site -- @param #SHORAD self -- @param #string TargetGroupName Name of the target group -- @return #boolean Returns true for a match, else false function SHORAD:_CheckShotAtSams(TargetGroupName) + self:T(self.lid .. " _CheckShotAtSams") local tgtgrp = TargetGroupName local shorad = self.Samset --local shoradset = shorad:GetAliveSet() --#table @@ -376,11 +394,12 @@ do end return returnname end - + --- Calculate if the missile shot is detected -- @param #SHORAD self -- @return #boolean Returns true for a detection, else false function SHORAD:_ShotIsDetected() + self:T(self.lid .. " _ShotIsDetected") local IsDetected = false local DetectionProb = math.random(self.DefenseLowProb, self.DefenseHighProb) -- reference value local ActualDetection = math.random(1,100) -- value for this shot @@ -389,15 +408,15 @@ do end return IsDetected end - + --- Wake up #SHORADs in a zone with diameter Radius for ActiveTimer seconds -- @param #SHORAD self -- @param #string TargetGroup Name of the target group used to build the #ZONE -- @param #number Radius Radius of the #ZONE -- @param #number ActiveTimer Number of seconds to stay active -- @param #number TargetCat (optional) Category, i.e. Object.Category.UNIT or Object.Category.STATIC - -- @usage Use this function to integrate with other systems, example - -- + -- @usage Use this function to integrate with other systems, example + -- -- local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart() -- myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue") -- myshorad:SwitchDebug(true) @@ -405,6 +424,7 @@ do -- mymantis:AddShorad(myshorad,720) -- mymantis:Start() function SHORAD:WakeUpShorad(TargetGroup, Radius, ActiveTimer, TargetCat) + self:T(self.lid .. " WakeUpShorad") self:T({TargetGroup, Radius, ActiveTimer, TargetCat}) local targetcat = TargetCat or Object.Category.UNIT local targetgroup = TargetGroup @@ -442,7 +462,7 @@ do self:T(text) local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug) if self.UseEmOnOff then - _group:SetAIOn() + --_group:SetAIOn() _group:EnableEmission(true) end _group:OptionAlarmStateRed() @@ -454,14 +474,15 @@ do end end end + return self end - + --- Main function - work on the EventData -- @param #SHORAD self -- @param Core.Event#EVENTDATA EventData The event details table data set - function SHORAD:OnEventShot( EventData ) + function SHORAD:HandleEventShot( EventData ) self:T( { EventData } ) - + self:T(self.lid .. " HandleEventShot") --local ShootingUnit = EventData.IniDCSUnit --local ShootingUnitName = EventData.IniDCSUnitName local ShootingWeapon = EventData.Weapon -- Identify the weapon fired @@ -473,7 +494,7 @@ do local IsDetected = self:_ShotIsDetected() -- convert to text local DetectedText = "false" - if IsDetected then + if IsDetected then DetectedText = "true" end local text = string.format("%s Missile Launched = %s | Detected probability state is %s", self.lid, ShootingWeaponName, DetectedText) @@ -490,7 +511,7 @@ do targetunit = UNIT:Find(targetdata) elseif targetcat == Object.Category.STATIC then -- STATIC targetunit = STATIC:Find(targetdata) - end + end --local targetunitname = Unit.getName(targetdata) -- Unit name if targetunit and targetunit:IsAlive() then local targetunitname = targetunit:GetName() @@ -507,7 +528,7 @@ do local text = string.format("%s Missile Target = %s", self.lid, tostring(targetgroupname)) self:T( text ) local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug) - -- check if we or a SAM site are the target + -- check if we or a SAM site are the target --local TargetGroup = EventData.TgtGroup -- Wrapper.Group#GROUP local shotatus = self:_CheckShotAtShorad(targetgroupname) --#boolean local shotatsams = self:_CheckShotAtSams(targetgroupname) --#boolean @@ -516,12 +537,12 @@ do self:T({shotatsams=shotatsams,shotatus=shotatus}) self:WakeUpShorad(targetgroupname, self.Radius, self.ActiveTimer, targetcat) end - end + end end end - end + end -- end ----------------------------------------------------------------------- -- SHORAD end ------------------------------------------------------------------------ +----------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 3133cd715..c3a45c8c5 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -11639,7 +11639,7 @@ end --- Get wind speed on carrier deck parallel and perpendicular to runway. -- @param #AIRBOSS self --- @param #number alt Altitude in meters. Default 50 m. +-- @param #number alt Altitude in meters. Default 15 m. (change made from 50m from Discord discussion from Sickdog) -- @return #number Wind component parallel to runway im m/s. -- @return #number Wind component perpendicular to runway in m/s. -- @return #number Total wind strength in m/s. @@ -11662,7 +11662,7 @@ function AIRBOSS:GetWindOnDeck(alt) zc=UTILS.Rotate2D(zc, -self.carrierparam.rwyangle) -- Wind (from) vector - local vw=cv:GetWindWithTurbulenceVec3(alt or 50) + local vw=cv:GetWindWithTurbulenceVec3(alt or 15) -- Total wind velocity vector. -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index d243475b0..8b718f7be 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -1,3 +1,4 @@ + --- **Ops** -- Combat Search and Rescue. -- -- === @@ -212,8 +213,11 @@ CSAR = { -- @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 +-- --- Updated and sorted list of known NDB beacons (in kHz!) from the available maps. + +--[[ Moved to Utils -- @field #CSAR.SkipFrequencies CSAR.SkipFrequencies = { 214,274,291.5,295,297.5, @@ -229,7 +233,8 @@ CSAR.SkipFrequencies = { 905,907,920,935,942,950,995, 1000,1025,1030,1050,1065,1116,1175,1182,1210 } - +--]] + --- All slot / Limit settings -- @type CSAR.AircraftType -- @field #string typename Unit type name. @@ -239,13 +244,14 @@ 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-8MTV2"] = 12 +CSAR.AircraftType["Mi-8MT"] = 12 CSAR.AircraftType["Mi-24P"] = 8 CSAR.AircraftType["Mi-24V"] = 8 --- CSAR class version. -- @field #string version -CSAR.version="0.1.8r1" +CSAR.version="0.1.8r3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -317,6 +323,7 @@ function CSAR:New(Coalition, Template, Alias) 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 @@ -361,7 +368,7 @@ function CSAR:New(Coalition, Template, Alias) self.mashprefix = {"MASH"} -- prefixes used to find MASHes self.mash = SET_GROUP:New():FilterCoalitions(self.coalition):FilterPrefixes(self.mashprefix):FilterOnce() -- currently only GROUP objects, maybe support STATICs also? self.autosmoke = false -- automatically smoke location when heli is near - self.autosmokedistance = 1000 -- distance for autosmoke + self.autosmokedistance = 2000 -- distance for autosmoke -- added 0.1.4 self.limitmaxdownedpilots = true self.maxdownedpilots = 25 @@ -458,6 +465,14 @@ function CSAR:New(Coalition, Template, Alias) -- @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 @@ -491,6 +506,7 @@ function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Descript DownedPilot.typename = Typename or "" DownedPilot.group = Group DownedPilot.timestamp = 0 + DownedPilot.alive = true -- Add Pilot local PilotTable = self.downedPilots @@ -609,7 +625,8 @@ end -- @param #number _freq Frequency -- @param #boolean noMessage -- @param #string _description Description -function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _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}) @@ -622,11 +639,10 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq) - local _typeName = _typeName or "PoW" + local _typeName = _typeName or "Pilot" if not noMessage then self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, 10) - --local m = MESSAGE:New("MAYDAY MAYDAY! " .. _typeName .. " is down. ",10,"INFO"):ToCoalition(self.coalition) end if _freq then @@ -635,15 +651,14 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla self:_AddSpecialOptions(_spawnedGroup) - local _text = " " - if _playerName ~= nil then - _text = "Pilot " .. _playerName .. " of " .. _unitName .. " - " .. _typeName - elseif _typeName ~= nil then - _text = "AI Pilot of " .. _unitName .. " - " .. _typeName - else - _text = _description - end - + local _text = _description + if not forcedesc then + if _playerName ~= nil then + _text = "Pilot " .. _playerName + elseif _unitName ~= nil then + _text = "AI Pilot of " .. _unitName + end + end self:T({_spawnedGroup, _alias}) local _GroupName = _spawnedGroup:GetName() or _alias @@ -662,7 +677,10 @@ end -- @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. -function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage) +-- @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 = ZONE:New(_zone) -- trigger to use as reference position @@ -671,7 +689,9 @@ function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _ return end - local _description = _description or "Unknown" + local _description = _description or "PoW" + local unitname = unitname or "Old Rusty" + local typename = typename or "Phantom II" local pos = {} if _randomPoint then @@ -690,7 +710,7 @@ function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _ _country = country.id.UN_PEACEKEEPERS end - self:_AddCsar(_coalition, _country, pos, "PoW", _description, nil, freq, _nomessage, _description) + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) return self end @@ -702,12 +722,15 @@ end -- @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. --- @usage 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: +-- @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, "Pilot Wagner", true ) -function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage) - self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage) +-- 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 @@ -865,7 +888,11 @@ function CSAR:_EventHandler(EventData) end if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then - self:_RescuePilots(_unit) + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_event.IniUnitName) then + self:_DisplayMessageToSAR(_unit, "Open the door to let me out!", self.messageTime, true) + else + self:_RescuePilots(_unit) + end else self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) end @@ -916,7 +943,7 @@ function CSAR:_CheckNameInDownedPilots(name) local found = false local table = nil for _,_pilot in pairs(PilotTable) do - if _pilot.name == name then + if _pilot.name == name and _pilot.alive == true then found = true table = _pilot break @@ -933,25 +960,10 @@ end function CSAR:_RemoveNameFromDownedPilots(name,force) local PilotTable = self.downedPilots --#CSAR.DownedPilot local found = false - for _,_pilot in pairs(PilotTable) do + for _index,_pilot in pairs(PilotTable) do if _pilot.name == name then - local group = _pilot.group -- Wrapper.Group#GROUP - if group then - if (not group:IsAlive()) or ( force == true) then -- don\'t delete groups which still exist - found = true - _pilot.desc = nil - _pilot.frequency = nil - _pilot.index = nil - _pilot.name = nil - _pilot.originalUnit = nil - _pilot.player = nil - _pilot.side = nil - _pilot.typename = nil - _pilot.group = nil - _pilot.timestamp = nil - end + self.downedPilots[_index].alive = false end - end end return found end @@ -974,7 +986,7 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) end local _woundedGroup = _downedpilot.group - if _woundedGroup ~= nil then + 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 @@ -986,11 +998,15 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self:T("...helinunit nil!") return end - - --if self:_CheckGroupNotKIA(_woundedGroup, _woundedGroupName, _heliUnit, _heliName) then + 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 @@ -998,14 +1014,35 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self:__Approach(-5,heliname,woundedgroupname) end elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then - self.heliVisibleMessage[_lookupKeyHeli] = nil + -- 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! Damn, that thing is loud!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud!\nRequest a flare or smoke if you need.", _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?!") - self:_RemoveNameFromDownedPilots(_downedpilot.name) + if not _downedpilot.alive then + --self:__KIA(1,_downedpilot.name) + self:_RemoveNameFromDownedPilots(_downedpilot.name, true) + end end return self end @@ -1106,27 +1143,27 @@ function CSAR:_IsLoadingDoorOpen( unit_name ) local type_name = unit:getTypeName() if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then - self:I(unit_name .. " Cargo doors are open or cargo door not present") + self:T(unit_name .. " Cargo doors are open or cargo door not present") ret_val = true end if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then - self:I(unit_name .. " a side door is open") + self:T(unit_name .. " a side door is open") ret_val = true end if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then - self:I(unit_name .. " a side door is open ") + self:T(unit_name .. " a side door is open ") ret_val = true end if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then - self:I(unit_name .. " front door(s) are open") + self:T(unit_name .. " front door(s) are open") ret_val = true end if ret_val == false then - self:I(unit_name .. " all doors are closed") + self:T(unit_name .. " all doors are closed") end return ret_val @@ -1155,27 +1192,13 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG local _reset = true - if (self.autosmoke == true) and (_distance < self.autosmokedistance) then - self:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) - end - - if self.heliVisibleMessage[_lookupKeyHeli] == nil then - if self.autosmoke == true then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud! Land or hover by the smoke.", _heliName, _pilotName), self.messageTime,true,true) - else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud! Request a Flare or Smoke if you need", _heliName, _pilotName), self.messageTime,true,true) - end - --mark as shown for THIS heli and THIS group - self.heliVisibleMessage[_lookupKeyHeli] = true - end - 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.", _heliName, _pilotName), self.messageTime,true,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", _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 ", _heliName, _pilotName), self.messageTime,true,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", _heliName, _pilotName), self.messageTime,false,true) end --mark as shown for THIS heli and THIS group self.heliCloseMessage[_lookupKeyHeli] = true @@ -1192,14 +1215,14 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) _time = self.landedStatus[_lookupKeyHeli] self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) - self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, true) + 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 self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in, bugger!", self.messageTime, true) + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) return true else self.landedStatus[_lookupKeyHeli] = nil @@ -1211,7 +1234,7 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG else if (_distance < self.loadDistance) then if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in, honk!", self.messageTime, true) + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) return true else self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) @@ -1252,7 +1275,7 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG 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 not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in, noob!", self.messageTime, true) + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) return true else self.hoverStatus[_lookupKeyHeli] = nil @@ -1281,36 +1304,6 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG end end ---- (Internal) Check if group not KIA. --- @param #CSAR self --- @param Wrapper.Group#GROUP _woundedGroup --- @param #string _woundedGroupName --- @param Wrapper.Unit#UNIT _heliUnit --- @param #string _heliName --- @return #boolean Outcome -function CSAR:_CheckGroupNotKIA(_woundedGroup, _woundedGroupName, _heliUnit, _heliName) - self:T(self.lid .. " _CheckGroupNotKIA") - -- check if unit has died or been picked up - local inTransit = false - if _woundedGroup and _heliUnit then - for _currentHeli, _groups in pairs(self.inTransitGroups) do - if _groups[_woundedGroupName] then - inTransit = true - self:_DisplayToAllSAR(string.format("%s has been picked up by %s", _woundedGroupName, _currentHeli), self.coalition, self.messageTime) - break - end -- end name check - end -- end loop - if not inTransit then - -- KIA - self:_DisplayToAllSAR(string.format("%s is KIA ", _woundedGroupName), self.coalition, self.messageTime) - end - --stops the message being displayed again - self:_RemoveNameFromDownedPilots(_woundedGroupName) - end - --continue - return inTransit -end - --- (Internal) Monitor in-flight returning groups. -- @param #CSAR self -- @param #string heliname Heli name @@ -1339,8 +1332,12 @@ function CSAR:_ScheduledSARFlight(heliname,groupname) end if _dist < 200 and _heliUnit:InAir() == false then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(heliname) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) + else self:_RescuePilots(_heliUnit) return + end end --queue up @@ -1428,12 +1425,8 @@ function CSAR:_GetPositionOfWounded(_woundedGroup) _coordinatesText = _coordinate:ToStringLLDMS() elseif self.coordtype == 2 then -- MGRS _coordinatesText = _coordinate:ToStringMGRS() - elseif self.coordtype == 3 then -- Bullseye Imperial - local Settings = _SETTINGS:SetImperial() - _coordinatesText = _coordinate:ToStringBULLS(self.coalition,Settings) - else -- Bullseye Metric --(medevac.coordtype == 4) - local Settings = _SETTINGS:SetMetric() - _coordinatesText = _coordinate:ToStringBULLS(self.coalition,Settings) + else -- Bullseye Metric --(medevac.coordtype == 4 or 3) + _coordinatesText = _coordinate:ToStringBULLS(self.coalition) end end return _coordinatesText @@ -1461,18 +1454,17 @@ function CSAR:_DisplayActiveSAR(_unitName) self:T({Table=_value}) --local _woundedGroup = GROUP:FindByName(_groupName) local _woundedGroup = _value.group - if _woundedGroup then + 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}) - -- change distance to miles if self.coordtype < 4 local distancetext = "" - if self.coordtype < 4 then - distancetext = string.format("%.3fnm",UTILS.MetersToNM(_distance)) + if _SETTINGS:IsImperial() then + distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) else - distancetext = string.format("%.3fkm", _distance/1000.0) + distancetext = string.format("%.1fkm", _distance/1000.0) end table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) end @@ -1537,15 +1529,16 @@ function CSAR:_SignalFlare(_unitName) end local _closest = self:_GetClosestDownedPilot(_heli) - - if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < 8000.0 then + local smokedist = 8000 + if self.approachdist_far > smokedist then smokedist = self.approachdist_far end + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) local _distance = 0 - if self.coordtype < 4 then - _distance = string.format("%.3fnm",UTILS.MetersToNM(_closest.distance)) + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) else - _distance = string.format("%.3fkm",_closest.distance) + _distance = string.format("%.1fkm",_closest.distance) end local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) @@ -1553,11 +1546,13 @@ function CSAR:_SignalFlare(_unitName) local _coord = _closest.pilot:GetCoordinate() _coord:FlareRed(_clockDir) else - local disttext = "4.3nm" - if self.coordtype == 4 then - disttext = "8km" - end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",disttext), self.messageTime) + 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) end return self end @@ -1589,14 +1584,16 @@ function CSAR:_Reqsmoke( _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 < 8000.0 then + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) local _distance = 0 - if self.coordtype < 4 then - _distance = string.format("%.3fnm",UTILS.MetersToNM(_closest.distance)) + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) else - _distance = string.format("%.3fkm",_closest.distance) + _distance = string.format("%.1fkm",_closest.distance) end local _msg = string.format("%s - Popping signal smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) @@ -1604,11 +1601,13 @@ function CSAR:_Reqsmoke( _unitName ) local color = self.smokecolor _coord:Smoke(color) else - local disttext = "4.3nm" - if self.coordtype == 4 then - disttext = "8km" - end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",disttext), self.messageTime) + 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) end return self end @@ -1753,70 +1752,10 @@ end -- @param #CSAR self function CSAR:_GenerateVHFrequencies() self:T(self.lid .. " _GenerateVHFrequencies") - local _skipFrequencies = self.SkipFrequencies + --local _skipFrequencies = self.SkipFrequencies local FreeVHFFrequencies = {} - local UsedVHFFrequencies = {} - - -- first range - local _start = 200000 - while _start < 400000 do - - -- skip existing NDB frequencies - local _found = false - for _, value in pairs(_skipFrequencies) do - if value * 1000 == _start then - _found = true - break - end - end - - if _found == false then - table.insert(FreeVHFFrequencies, _start) - end - - _start = _start + 10000 - end - - -- second range - _start = 400000 - while _start < 850000 do - - -- skip existing NDB frequencies - local _found = false - for _, value in pairs(_skipFrequencies) do - if value * 1000 == _start then - _found = true - break - end - end - - if _found == false then - table.insert(FreeVHFFrequencies, _start) - end - - _start = _start + 10000 - end - - -- third range - _start = 850000 - while _start <= 999000 do -- updated for Gazelle - - -- skip existing NDB frequencies - local _found = false - for _, value in pairs(_skipFrequencies) do - if value * 1000 == _start then - _found = true - break - end - end - - if _found == false then - table.insert(FreeVHFFrequencies, _start) - end - - _start = _start + 50000 - end + FreeVHFFrequencies = UTILS.GenerateVHFrequencies() self.FreeVHFFrequencies = FreeVHFFrequencies return self end @@ -1853,7 +1792,8 @@ function CSAR:_GetClockDirection(_heli, _group) if _heading then local Aspect = Angle - _heading if Aspect == 0 then Aspect = 360 end - clock = math.floor(Aspect / 30) + --clock = math.floor(Aspect / 30) + clock = math.abs(UTILS.Round((Aspect / 30),0)) if clock == 0 then clock = 12 end end return clock @@ -1913,8 +1853,8 @@ function CSAR:_CountActiveDownedPilots() self:T(self.lid .. " _CountActiveDownedPilots") local PilotsInFieldN = 0 for _, _unitName in pairs(self.downedPilots) do - self:T({_unitName}) - if _unitName.name ~= nil then + self:T({_unitName.desc}) + if _unitName.alive == true then PilotsInFieldN = PilotsInFieldN + 1 end end @@ -1965,6 +1905,26 @@ function CSAR:onafterStart(From, Event, To) return self end +--- (Internal) Function called before Status() event. +-- @param #CSAR self +function CSAR:_CheckDownedPilotTable() + local pilots = self.downedPilots + for _,_entry in pairs (pilots) do + self:T("Checking for " .. _entry.name) + self:T({entry=_entry}) + local group = _entry.group + if not group:IsAlive() then + self:T("Group is dead") + if _entry.alive == true then + self:T("Switching .alive to false") + self:__KIA(1,_entry.desc) + self:_RemoveNameFromDownedPilots(_entry.name,true) + end + end + end + return self +end + --- (Internal) Function called before Status() event. -- @param #CSAR self. -- @param #string From From state. @@ -1975,15 +1935,18 @@ function CSAR:onbeforeStatus(From, Event, To) -- housekeeping self:_AddMedevacMenuItem() self:_RefreshRadioBeacons() + self:_CheckDownedPilotTable() for _,_sar in pairs (self.csarUnits) do local PilotTable = self.downedPilots for _,_entry in pairs (PilotTable) do - 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) + 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 diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 4c9adf68d..3ab505402 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1,4 +1,4 @@ ---- **Ops** -- Combat Troops & Logistics Deployment. +--- **Ops** -- Combat Troops & Logistics Department. -- -- === -- @@ -59,6 +59,7 @@ CTLD_CARGO = { ["TROOPS"] = "Troops", -- #string troops ["FOB"] = "FOB", -- #string FOB ["CRATE"] = "Crate", -- #string crate + ["REPAIR"] = "Repair", -- #string repair } --- Function to create new CTLD_CARGO object. @@ -81,8 +82,8 @@ CTLD_CARGO = { self.Name = Name or "none" -- #string self.Templates = Templates or {} -- #table self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum - self.HasBeenMoved = HasBeenMoved or false -- #booolean - self.LoadDirectly = LoadDirectly or false -- #booolean + self.HasBeenMoved = HasBeenMoved or false -- #boolean + self.LoadDirectly = LoadDirectly or false -- #boolean self.CratesNeeded = CratesNeeded or 0 -- #number self.Positionable = Positionable or nil -- Wrapper.Positionable#POSITIONABLE self.HasBeenDropped = Dropped or false --#boolean @@ -169,13 +170,25 @@ CTLD_CARGO = { return false end end + --- Set WasDropped. -- @param #CTLD_CARGO self -- @param #boolean dropped function CTLD_CARGO:SetWasDropped(dropped) self.HasBeenDropped = dropped or false end - + + --- Query crate type for REPAIR + -- @param #CTLD_CARGO self + -- @param #boolean + function CTLD_CARGO:IsRepair() + if self.CargoType == "Repair" then + return true + else + return false + end + end + end do @@ -235,6 +248,9 @@ do -- -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build: -- my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4) -- +-- -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair +-- my_ctld:AddCratesRepair("Humvee Repair","Humvee",CTLD_CARGO.Enum.REPAIR,1) +-- -- ## 1.3 Add logistics zones -- -- Add zones for loading troops and crates and dropping, building crates @@ -262,6 +278,7 @@ do -- -- my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD. -- my_ctld.CrateDistance = 30 -- List and Load crates in this radius only. +-- my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere. -- my_ctld.maximumHoverHeight = 15 -- Hover max this high to load. -- my_ctld.minimumHoverHeight = 4 -- Hover min this low to load. -- my_ctld.forcehoverload = true -- Crates (not: troops) can only be loaded while hovering. @@ -271,6 +288,7 @@ do -- my_ctld.movetroopsdistance = 5000 -- .. but only if this far away (in meters) -- my_ctld.smokedistance = 2000 -- Only smoke or flare zones if requesting player unit is this far away (in meters) -- my_ctld.suppressmessages = false -- Set to true if you want to script your own messages. +-- my_ctld.repairtime = 300 -- Number of seconds it takes to repair a unit. -- -- ## 2.1 User functions -- @@ -289,7 +307,7 @@ do -- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, -- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, -- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, --- ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, +-- ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, -- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, -- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, -- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, @@ -335,8 +353,16 @@ do -- function my_ctld:OnAfterTroopsDeployed(From, Event, To, Group, Unit, Troops) -- ... your code here ... -- end +-- +-- ## 3.4 OnAfterTroopsExtracted -- --- ## 3.4 OnAfterCratesDropped +-- This function is called when a player has re-boarded already deployed troops from the field: +-- +-- function my_ctld:OnAfterTroopsExtracted(From, Event, To, Group, Unit, Troops) +-- ... your code here ... +-- end +-- +-- ## 3.5 OnAfterCratesDropped -- -- This function is called when a player has deployed crates to a DROP zone: -- @@ -344,15 +370,19 @@ do -- ... your code here ... -- end -- --- ## 3.5 OnAfterCratesBuild +-- ## 3.6 OnAfterCratesBuild, OnAfterCratesRepaired -- -- This function is called when a player has build a vehicle or FOB: -- -- function my_ctld:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) -- ... your code here ... +-- end +-- +-- function my_ctld:OnAfterCratesRepaired(From, Event, To, Group, Unit, Vehicle) +-- ... your code here ... -- end -- --- ## 3.6 A simple SCORING example: +-- ## 3.7 A simple SCORING example: -- -- To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport) -- @@ -376,11 +406,12 @@ do -- -- ## 4.1 Manage Crates -- --- Use this entry to get, load, list nearby, drop, and build crates. Also @see options. +-- Use this entry to get, load, list nearby, drop, build and repair crates. Also @see options. -- -- ## 4.2 Manage Troops -- --- Use this entry to load and drop troops. +-- Use this entry to load, drop and extract troops. NOTE - with extract you can only load troops from the field that were deployed prior. +-- Currently limited CTLD_CARGO troops, which are build from **one** template. Also, this will heal/complete your units as they are respawned. -- -- ## 4.3 List boarded cargo -- @@ -410,6 +441,7 @@ do -- my_ctld.enableHercules = true -- my_ctld.HercMinAngels = 155 -- for troop/cargo drop via chute in meters, ca 470 ft -- my_ctld.HercMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft +-- my_ctld.HercMaxSpeed = 77 -- 77mps or 270 kph or 150 kn -- -- Also, the following options need to be set to `true`: -- @@ -478,7 +510,7 @@ CTLD = { -- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon --- Zone Type Info. --- @type CTLD. +-- @type CTLD.CargoZoneType CTLD.CargoZoneType = { LOAD = "load", DROP = "drop", @@ -508,32 +540,16 @@ CTLD.UnitTypes = { ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, + ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers } ---- Updated and sorted known NDB beacons (in kHz!) from the available maps --- @field #CTLD.SkipFrequencies -CTLD.SkipFrequencies = { - 214,274,291.5,295,297.5, - 300.5,304,307,309.5,311,312,312.5,316, - 320,324,328,329,330,336,337, - 342,343,348,351,352,353,358, - 363,365,368,372.5,374, - 380,381,384,389,395,396, - 414,420,430,432,435,440,450,455,462,470,485, - 507,515,520,525,528,540,550,560,570,577,580,602,625,641,662,670,680,682,690, - 705,720,722,730,735,740,745,750,770,795, - 822,830,862,866, - 905,907,920,935,942,950,995, - 1000,1025,1030,1050,1065,1116,1175,1182,1210 - } - --- CTLD class version. -- @field #string version -CTLD.version="0.1.3r2" +CTLD.version="0.1.4r3" --- Instantiate a new CTLD. -- @param #CTLD self @@ -591,11 +607,13 @@ function CTLD:New(Coalition, Prefixes, Alias) self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- CTLD status update. self:AddTransition("*", "TroopsPickedUp", "*") -- CTLD pickup event. + self:AddTransition("*", "TroopsExtracted", "*") -- CTLD extract event. self:AddTransition("*", "CratesPickedUp", "*") -- CTLD pickup event. self:AddTransition("*", "TroopsDeployed", "*") -- CTLD deploy event. self:AddTransition("*", "TroopsRTB", "*") -- CTLD deploy event. self:AddTransition("*", "CratesDropped", "*") -- CTLD deploy event. - self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. + self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. + self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- tables @@ -609,7 +627,6 @@ function CTLD:New(Coalition, Prefixes, Alias) self.UsedVHFFrequencies = {} self.UsedUHFFrequencies = {} self.UsedFMFrequencies = {} - --self.jtacGeneratedLaserCodes = {} -- radio beacons self.RadioSound = "beacon.ogg" @@ -642,6 +659,7 @@ function CTLD:New(Coalition, Prefixes, Alias) self.minimumHoverHeight = 4 self.forcehoverload = true self.hoverautoloading = true + self.dropcratesanywhere = false -- #1570 self.smokedistance = 2000 self.movetroopstowpzone = true @@ -651,10 +669,14 @@ function CTLD:New(Coalition, Prefixes, Alias) self.enableHercules = false self.HercMinAngels = 165 -- for troop/cargo drop via chute self.HercMaxAngels = 2000 -- for troop/cargo drop via chute + self.HercMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps -- message suppression self.suppressmessages = false + -- time to repair a unit/group + self.repairtime = 300 + for i=1,100 do math.random() end @@ -662,7 +684,6 @@ function CTLD:New(Coalition, Prefixes, Alias) self:_GenerateVHFrequencies() self:_GenerateUHFrequencies() self:_GenerateFMFrequencies() - --self:_GenerateLaserCodes() -- curr unused ------------------------ --- Pseudo Functions --- @@ -705,6 +726,17 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #CTLD_CARGO Cargo Cargo troops. -- @return #CTLD self + --- FSM Function OnAfterTroopsExtracted. + -- @function [parent=#CTLD] OnAfterTroopsExtracted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo troops. + -- @return #CTLD self + --- FSM Function OnAfterCratesPickedUp. -- @function [parent=#CTLD] OnAfterCratesPickedUp -- @param #CTLD self @@ -738,7 +770,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. -- @return #CTLD self - --- FSM Function OnAfterCratesBuild. + --- FSM Function OnAfterCratesBuild. -- @function [parent=#CTLD] OnAfterCratesBuild -- @param #CTLD self -- @param #string From State. @@ -748,7 +780,18 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. -- @return #CTLD self - + + --- FSM Function OnAfterCratesRepaired. + -- @function [parent=#CTLD] OnAfterCratesRepaired + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired. + -- @return #CTLD self + --- FSM Function OnAfterTroopsRTB. -- @function [parent=#CTLD] OnAfterTroopsRTB -- @param #CTLD self @@ -792,37 +835,16 @@ end function CTLD:_GenerateUHFrequencies() self:T(self.lid .. " _GenerateUHFrequencies") self.FreeUHFFrequencies = {} - local _start = 220000000 - - while _start < 399000000 do - table.insert(self.FreeUHFFrequencies, _start) - _start = _start + 500000 - end - + self.FreeUHFFrequencies = UTILS.GenerateUHFrequencies() return self end --- (Internal) Function to generate valid FM Frequencies --- @param #CTLD sel +-- @param #CTLD self function CTLD:_GenerateFMFrequencies() self:T(self.lid .. " _GenerateFMrequencies") self.FreeFMFrequencies = {} - local _start = 220000000 - - while _start < 399000000 do - - _start = _start + 500000 - end - - for _first = 3, 7 do - for _second = 0, 5 do - for _third = 0, 9 do - local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit - table.insert(self.FreeFMFrequencies, _frequency) - end - end - end - + self.FreeFMFrequencies = UTILS.GenerateFMFrequencies() return self end @@ -830,111 +852,16 @@ end -- @param #CTLD self function CTLD:_GenerateVHFrequencies() self:T(self.lid .. " _GenerateVHFrequencies") - local _skipFrequencies = self.SkipFrequencies - - self.FreeVHFFrequencies = {} - self.UsedVHFFrequencies = {} - - -- first range - local _start = 200000 - while _start < 400000 do - - -- skip existing NDB frequencies# - local _found = false - for _, value in pairs(_skipFrequencies) do - if value * 1000 == _start then - _found = true - break - end - end - if _found == false then - table.insert(self.FreeVHFFrequencies, _start) - end - _start = _start + 10000 - end - - -- second range - _start = 400000 - while _start < 850000 do - -- skip existing NDB frequencies - local _found = false - for _, value in pairs(_skipFrequencies) do - if value * 1000 == _start then - _found = true - break - end - end - if _found == false then - table.insert(self.FreeVHFFrequencies, _start) - end - _start = _start + 10000 - end - - -- third range - _start = 850000 - while _start <= 999000 do -- adjusted for Gazelle - -- skip existing NDB frequencies - local _found = false - for _, value in pairs(_skipFrequencies) do - if value * 1000 == _start then - _found = true - break - end - end - if _found == false then - table.insert(self.FreeVHFFrequencies, _start) - end - _start = _start + 50000 - end - + self.FreeVHFFrequencies = {} + self.UsedVHFFrequencies = {} + self.FreeVHFFrequencies = UTILS.GenerateVHFrequencies() return self end ---- (Internal) Function to generate valid laser codes. --- @param #CTLD self -function CTLD:_GenerateLaserCodes() - self:T(self.lid .. " _GenerateLaserCodes") - self.jtacGeneratedLaserCodes = {} - -- generate list of laser codes - local _code = 1111 - local _count = 1 - while _code < 1777 and _count < 30 do - while true do - _code = _code + 1 - if not self:_ContainsDigit(_code, 8) - and not self:_ContainsDigit(_code, 9) - and not self:_ContainsDigit(_code, 0) then - table.insert(self.jtacGeneratedLaserCodes, _code) - break - end - end - _count = _count + 1 - end -end - ---- (Internal) Helper function to generate laser codes. --- @param #CTLD self --- @param #number _number --- @param #number _numberToFind -function CTLD:_ContainsDigit(_number, _numberToFind) - self:T(self.lid .. " _ContainsDigit") - local _thisNumber = _number - local _thisDigit = 0 - while _thisNumber ~= 0 do - _thisDigit = _thisNumber % 10 - _thisNumber = math.floor(_thisNumber / 10) - if _thisDigit == _numberToFind then - return true - end - end - return false -end - --- (Internal) Event handler function -- @param #CTLD self -- @param Core.Event#EVENTDATA EventData function CTLD:_EventHandler(EventData) - -- TODO: events dead and playerleaveunit - nil table entries self:T(string.format("%s Event = %d",self.lid, EventData.id)) local event = EventData -- Core.Event#EVENTDATA if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then @@ -953,6 +880,7 @@ function CTLD:_EventHandler(EventData) -- Herc support --self:T_unit:GetTypeName()) if _unit:GetTypeName() == "Hercules" and self.enableHercules then + self.Loaded_Cargo[unitname] = nil self:_RefreshF10Menus() end return @@ -996,7 +924,6 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) if not self.debug then return self end elseif not grounded and not hoverload then self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) - --local m = MESSAGE:New("You need to land or hover in position to load!",15,"CTLD"):ToGroup(Group) if not self.debug then return self end end -- load troops into heli @@ -1005,10 +932,8 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) local unitname = unit:GetName() local cargotype = Cargotype -- #CTLD_CARGO local cratename = cargotype:GetName() -- #string - --self:Tself.lid .. string.format("Troops %s requested", cratename)) -- see if this heli can load troops local unittype = unit:GetTypeName() - --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities local capabilities = self:_GetUnitCapabilities(Unit) local cantroops = capabilities.troops -- #boolean local trooplimit = capabilities.trooplimit -- #number @@ -1027,7 +952,6 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) end if troopsize + numberonboard > trooplimit then self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) - --local m = MESSAGE:New("Sorry, we\'re crammed already!",10,"CTLD",true):ToGroup(group) return else self.CargoCounter = self.CargoCounter + 1 @@ -1037,12 +961,206 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) table.insert(loaded.Cargo,loadcargotype) self.Loaded_Cargo[unitname] = loaded self:_SendMessage("Troops boarded!", 10, false, Group) - --local m = MESSAGE:New("Troops boarded!",10,"CTLD",true):ToGroup(group) self:__TroopsPickedUp(1,Group, Unit, Cargotype) end return self end +function CTLD:_FindRepairNearby(Group, Unit, Repairtype) + self:T(self.lid .. " _FindRepairNearby") + local unitcoord = Unit:GetCoordinate() + + -- find nearest group of deployed groups + local nearestGroup = nil + local nearestGroupIndex = -1 + local nearestDistance = 10000000 + for k,v in pairs(self.DroppedTroops) do + local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + if distance < nearestDistance and distance ~= -1 then + nearestGroup = v + nearestGroupIndex = k + nearestDistance = distance + end + end + + -- found one and matching distance? + if nearestGroup == nil or nearestDistance > 1000 then + self:_SendMessage("No unit close enough to repair!", 10, false, Group) + return nil, nil + end + + local groupname = nearestGroup:GetName() + --self:I(string.format("***** Found Group %s",groupname)) + + -- helper to find matching template + local function matchstring(String,Table) + local match = false + if type(Table) == "table" then + for _,_name in pairs (Table) do + if string.find(String,_name) then + match = true + break + end + end + else + if type(String) == "string" then + if string.find(String,Table) then match = true end + end + end + return match + end + + -- walk through generics and find matching type + local Cargotype = nil + for k,v in pairs(self.Cargo_Crates) do + --self:I({groupname,v.Templates}) + if matchstring(groupname,v.Templates) and matchstring(groupname,Repairtype) then + Cargotype = v -- #CTLD_CARGO + break + end + end + + if Cargotype == nil then + --self:_SendMessage("Can't find a matching group for " .. Repairtype, 10, false, Group) + return nil, nil + else + return nearestGroup, Cargotype + end + +end + +--- (Internal) Function to repair an object. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #table Crates Table of #CTLD_CARGO objects near the unit. +-- @param #CTLD.Buildable Build Table build object. +-- @param #number Number Number of objects in Crates (found) to limit search. +function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number) + self:T(self.lid .. " _RepairObjectFromCrates") + local build = Build -- -- #CTLD.Buildable + --self:I({Build=Build}) + local Repairtype = build.Template -- #string + local NearestGroup, CargoType = self:_FindRepairNearby(Group,Unit,Repairtype) -- Wrapper.Group#GROUP, #CTLD_CARGO + --self:I({Repairtype=Repairtype, CargoType=CargoType, NearestGroup=NearestGroup}) + if NearestGroup ~= nil then + if self.repairtime < 2 then self.repairtime = 30 end -- noob catch + self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) + -- now we can build .... + --NearestGroup:Destroy(false) + local name = CargoType:GetName() + local required = CargoType:GetCratesNeeded() + local template = CargoType:GetTemplates() + local ctype = CargoType:GetType() + local object = {} -- #CTLD.Buildable + object.Name = CargoType:GetName() + object.Required = required + object.Found = required + object.Template = template + object.CanBuild = true + object.Type = ctype -- #CTLD_CARGO.Enum + self:_CleanUpCrates(Crates,Build,Number) + local desttimer = TIMER:New(function() NearestGroup:Destroy(false) end, self) + desttimer:Start(self.repairtime - 1) + local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true) + buildtimer:Start(self.repairtime) + --self:_BuildObjectFromCrates(Group,Unit,object) + else + self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) + end + return self +end + + --- (Internal) Function to extract (load from the field) troops into a heli. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + -- @param #CTLD_CARGO Cargotype + function CTLD:_ExtractTroops(Group, Unit) -- #1574 thanks to @bbirchnz! + self:T(self.lid .. " _ExtractTroops") + -- landed or hovering over load zone? + local grounded = not self:IsUnitInAir(Unit) + local hoverload = self:CanHoverLoad(Unit) + + if not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + end + -- load troops into heli + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + -- see if this heli can load troops + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) + local cantroops = capabilities.troops -- #boolean + local trooplimit = capabilities.trooplimit -- #number + local unitcoord = unit:GetCoordinate() + + -- find nearest group of deployed troops + local nearestGroup = nil + local nearestGroupIndex = -1 + local nearestDistance = 10000000 + for k,v in pairs(self.DroppedTroops) do + local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + if distance < nearestDistance and distance ~= -1 then + nearestGroup = v + nearestGroupIndex = k + nearestDistance = distance + end + end + + if nearestGroup == nil or nearestDistance > self.CrateDistance then + self:_SendMessage("No units close enough to extract!", 10, false, Group) + return self + end + -- find matching cargo type + local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") + local Cargotype = nil + for k,v in pairs(self.Cargo_Troops) do + if v.Name == groupType then + Cargotype = v + break + end + end + + if Cargotype == nil then + self:_SendMessage("Can't find a matching cargo type for " .. groupType, 10, false, Group) + return self + end + + local troopsize = Cargotype:GetCratesNeeded() -- #number + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Troopsloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + if troopsize + numberonboard > trooplimit then + self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + return + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded) + self:T({cargotype=loadcargotype}) + loaded.Troopsloaded = loaded.Troopsloaded + troopsize + table.insert(loaded.Cargo,loadcargotype) + self.Loaded_Cargo[unitname] = loaded + self:_SendMessage("Troops boarded!", 10, false, Group) + self:__TroopsExtracted(1,Group, Unit, nearestGroup) + + -- clean up: + table.remove(self.DroppedTroops, nearestGroupIndex) + nearestGroup:Destroy() + end + return self + end + --- (Internal) Function to spawn crates in front of the heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group @@ -1053,31 +1171,31 @@ end function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) self:T(self.lid .. " _GetCrates") local cgoname = Cargo:GetName() - --self:T{cgoname, number, drop}) -- check if we are in LOAD zone local inzone = false local drop = drop or false if not drop then inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) else - inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) + if self.dropcratesanywhere then -- #1570 + inzone = true + else + inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) + end end if not inzone then self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) - --local m = MESSAGE:New("You are not close enough to a logistics zone!",15,"CTLD"):ToGroup(Group) if not self.debug then return self end end - + -- avoid crate spam local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities - --local capabilities = self.UnitTypes[Unit:GetTypeName()] -- #CTLD.UnitCapabilities local canloadcratesno = capabilities.cratelimit local loaddist = self.CrateDistance or 30 local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist) if numbernearby >= canloadcratesno and not drop then self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group) - --local m = MESSAGE:New("There are enough crates nearby already! Take care of those first!",15,"CTLD"):ToGroup(Group) return self end -- spawn crates in front of helicopter @@ -1086,7 +1204,6 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) local number = number or cargotype:GetCratesNeeded() --#number local cratesneeded = cargotype:GetCratesNeeded() --#number local cratename = cargotype:GetName() - --self:Tself.lid .. string.format("Crate %s requested", cratename)) local cratetemplate = "Container"-- #string -- get position and heading of heli local position = Unit:GetCoordinate() @@ -1134,7 +1251,6 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) self:__CratesDropped(1, Group, Unit, droppedcargo) end self:_SendMessage(text, 10, false, Group) - --local m = MESSAGE:New(text,15,"CTLD",true):ToGroup(Group) return self end @@ -1153,7 +1269,6 @@ function CTLD:_ListCratesNearby( _group, _unit) for _,_entry in pairs (crates) do local entry = _entry -- #CTLD_CARGO local name = entry:GetName() --#string - -- TODO Meaningful sorting/aggregation local dropped = entry:WasDropped() if dropped then text:Add(string.format("Dropped crate for %s",name)) @@ -1162,14 +1277,12 @@ function CTLD:_ListCratesNearby( _group, _unit) end end if text:GetCount() == 1 then - text:Add("--------- N O N E ------------") + text:Add(" N O N E") end text:Add("------------------------------------------------------------") self:_SendMessage(text:Text(), 30, true, _group) - --local m = MESSAGE:New(text:Text(),15,"CTLD",true):ToGroup(_group) else self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist), 10, false, _group) - --local m = MESSAGE:New(string.format("No (loadable) crates within %d meters!",finddist),15,"CTLD",true):ToGroup(_group) end return self end @@ -1217,9 +1330,6 @@ function CTLD:_FindCratesNearby( _group, _unit, _dist) end end end - --self:Tstring.format("Found crates = %d",index)) - -- table.sort(found) - --self:T({found}) return found, index end @@ -1250,13 +1360,10 @@ function CTLD:_LoadCratesNearby(Group, Unit) ----------------------------------------- if not cancrates then self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) - --local m = MESSAGE:New("Sorry this chopper cannot carry crates!",10,"CTLD"):ToGroup(Group) elseif self.forcehoverload and not canhoverload then self:_SendMessage("Hover over the crates to pick them up!", 10, false, Group) - --local m = MESSAGE:New("Hover over the crates to pick them up!",10,"CTLD"):ToGroup(Group) elseif not grounded and not canhoverload then self:_SendMessage("Land or hover over the crates to pick them up!", 10, false, Group) - --local m = MESSAGE:New("Land or hover over the crates to pick them up!",10,"CTLD"):ToGroup(Group) else -- have we loaded stuff already? local numberonboard = 0 @@ -1275,7 +1382,6 @@ function CTLD:_LoadCratesNearby(Group, Unit) local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table if number == 0 or numberonboard == cratelimit then self:_SendMessage("Sorry no loadable crates nearby or fully loaded!", 10, false, Group) - --local m = MESSAGE:New("Sorry no loadable crates nearby or fully loaded!",10,"CTLD"):ToGroup(Group) return -- exit else -- go through crates and load @@ -1283,13 +1389,11 @@ function CTLD:_LoadCratesNearby(Group, Unit) local crateidsloaded = {} local loops = 0 while loaded.Cratesloaded < cratelimit and loops < number do - --for _ind,_crate in pairs (nearcrates) do loops = loops + 1 local crateind = 0 -- get crate with largest index for _ind,_crate in pairs (nearcrates) do if not _crate:HasMoved() and not _crate:WasDropped() and _crate:GetID() > crateind then - --crate = _crate crateind = _crate:GetID() end end @@ -1304,10 +1408,8 @@ function CTLD:_LoadCratesNearby(Group, Unit) crate:GetPositionable():Destroy() crate.Positionable = nil self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) - --local m = MESSAGE:New(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()),10,"CTLD"):ToGroup(Group) self:__CratesPickedUp(1, Group, Unit, crate) end - --if loaded.Cratesloaded == cratelimit then break end end self.Loaded_Cargo[unitname] = loaded -- clean up real world crates @@ -1339,7 +1441,6 @@ function CTLD:_ListCargo(Group, Unit) local unitname = Unit:GetName() local unittype = Unit:GetTypeName() local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities - --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities local trooplimit = capabilities.trooplimit -- #boolean local cratelimit = capabilities.cratelimit -- #number local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo @@ -1351,7 +1452,7 @@ function CTLD:_ListCargo(Group, Unit) report:Add("------------------------------------------------------------") report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit)) report:Add("------------------------------------------------------------") - report:Add("-- TROOPS --") + report:Add(" -- TROOPS --") for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum @@ -1360,10 +1461,10 @@ function CTLD:_ListCargo(Group, Unit) end end if report:GetCount() == 4 then - report:Add("--------- N O N E ------------") + report:Add(" N O N E") end report:Add("------------------------------------------------------------") - report:Add("-- CRATES --") + report:Add(" -- CRATES --") local cratecount = 0 for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO @@ -1374,15 +1475,13 @@ function CTLD:_ListCargo(Group, Unit) end end if cratecount == 0 then - report:Add("--------- N O N E ------------") + report:Add(" N O N E") end report:Add("------------------------------------------------------------") local text = report:Text() self:_SendMessage(text, 30, true, Group) - --local m = MESSAGE:New(text,30,"CTLD",true):ToGroup(Group) else self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d",trooplimit,cratelimit), 10, false, Group) - --local m = MESSAGE:New(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d",trooplimit,cratelimit),10,"CTLD"):ToGroup(Group) end return self end @@ -1456,13 +1555,11 @@ function CTLD:_UnloadTroops(Group, Unit) end -- template loop cargo:SetWasDropped(true) self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) - --local m = MESSAGE:New(string.format("Dropped Troops %s into action!",name),10,"CTLD"):ToGroup(Group) self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter]) end -- if type end end -- cargotable loop else -- droppingatbase self:_SendMessage("Troops have returned to base!", 10, false, Group) - --local m = MESSAGE:New("Troops have returned to base!",15,"CTLD"):ToGroup(Group) self:__TroopsRTB(1, Group, Unit) end -- cleanup load list @@ -1476,7 +1573,6 @@ function CTLD:_UnloadTroops(Group, Unit) local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum local dropped = cargo:WasDropped() - --local moved = cargo:HasMoved() if type ~= CTLD_CARGO.Enum.TROOP and not dropped then table.insert(loaded.Cargo,_cargo) loaded.Cratesloaded = loaded.Cratesloaded + 1 @@ -1487,10 +1583,8 @@ function CTLD:_UnloadTroops(Group, Unit) else if IsHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) - --local m = MESSAGE:New("Nothing loaded or not within airdrop parameters!",10,"CTLD"):ToGroup(Group) else self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) - --local m = MESSAGE:New("Nothing loaded or not hovering within parameters!",10,"CTLD"):ToGroup(Group) end end return self @@ -1502,13 +1596,15 @@ end -- @param Wrappe.Unit#UNIT Unit function CTLD:_UnloadCrates(Group, Unit) self:T(self.lid .. " _UnloadCrates") - -- check if we are in DROP zone - local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) - if not inzone then - self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) - --local m = MESSAGE:New("You are not close enough to a drop zone!",15,"CTLD"):ToGroup(Group) - if not self.debug then - return self + + if not self.dropcratesanywhere then -- #1570 + -- check if we are in DROP zone + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) + if not inzone then + self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) + if not self.debug then + return self + end end end -- check for hover unload @@ -1534,8 +1630,6 @@ function CTLD:_UnloadCrates(Group, Unit) self:_GetCrates(Group, Unit, cargo, 1, true) cargo:SetWasDropped(true) cargo:SetHasMoved(true) - --local name cargo:GetName() - --local m = MESSAGE:New(string.format("Dropped Crate for %s!",name),10,"CTLD"):ToGroup(Group) end end -- cleanup load list @@ -1557,10 +1651,8 @@ function CTLD:_UnloadCrates(Group, Unit) else if IsHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) - --local m = MESSAGE:New("Nothing loaded or not within airdrop parameters!",10,"CTLD"):ToGroup(Group) else self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) - --local m = MESSAGE:New("Nothing loaded or not hovering within parameters!",10,"CTLD"):ToGroup(Group) end end return self @@ -1582,7 +1674,7 @@ function CTLD:_BuildCrates(Group, Unit) -- get dropped crates for _,_crate in pairs(crates) do local Crate = _crate -- #CTLD_CARGO - if Crate:WasDropped() then + if Crate:WasDropped() and not Crate:IsRepair() then -- we can build these - maybe local name = Crate:GetName() local required = Crate:GetCratesNeeded() @@ -1621,7 +1713,6 @@ function CTLD:_BuildCrates(Group, Unit) if build.CanBuild then txtok = "YES" end - --self:T{name,needed,found,txtok}) local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok) report:Add(text) end -- end list buildables @@ -1629,7 +1720,6 @@ function CTLD:_BuildCrates(Group, Unit) report:Add("------------------------------------------------------------") local text = report:Text() self:_SendMessage(text, 30, true, Group) - --local m = MESSAGE:New(text,30,"CTLD",true):ToGroup(Group) -- let\'s get going if canbuild then -- loop again @@ -1643,7 +1733,84 @@ function CTLD:_BuildCrates(Group, Unit) end else self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) - --local m = MESSAGE:New(string.format("No crates within %d meters!",finddist),15,"CTLD",true):ToGroup(Group) + end -- number > 0 + return self +end + +--- (Internal) Function to repair nearby vehicles / FOBs +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrappe.Unit#UNIT Unit +function CTLD:_RepairCrates(Group, Unit) + self:T(self.lid .. " _RepairCrates") + -- get nearby crates + local finddist = self.CrateDistance or 30 + local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local buildables = {} + local foundbuilds = false + local canbuild = false + if number > 0 then + -- get dropped crates + for _,_crate in pairs(crates) do + local Crate = _crate -- #CTLD_CARGO + if Crate:WasDropped() and Crate:IsRepair() then + -- we can build these - maybe + local name = Crate:GetName() + local required = Crate:GetCratesNeeded() + local template = Crate:GetTemplates() + local ctype = Crate:GetType() + if not buildables[name] then + local object = {} -- #CTLD.Buildable + object.Name = name + object.Required = required + object.Found = 1 + object.Template = template + object.CanBuild = false + object.Type = ctype -- #CTLD_CARGO.Enum + buildables[name] = object + foundbuilds = true + else + buildables[name].Found = buildables[name].Found + 1 + foundbuilds = true + end + if buildables[name].Found >= buildables[name].Required then + buildables[name].CanBuild = true + canbuild = true + end + self:T({repair = buildables}) + end -- end dropped + end -- end crate loop + -- ok let\'s list what we have + local report = REPORT:New("Checklist Repairs") + report:Add("------------------------------------------------------------") + for _,_build in pairs(buildables) do + local build = _build -- Object table from above + local name = build.Name + local needed = build.Required + local found = build.Found + local txtok = "NO" + if build.CanBuild then + txtok = "YES" + end + local text = string.format("Type: %s | Required %d | Found %d | Can Repair %s", name, needed, found, txtok) + report:Add(text) + end -- end list buildables + if not foundbuilds then report:Add(" --- None Found ---") end + report:Add("------------------------------------------------------------") + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + -- let\'s get going + if canbuild then + -- loop again + for _,_build in pairs(buildables) do + local build = _build -- #CTLD.Buildable + if build.CanBuild then + self:_RepairObjectFromCrates(Group,Unit,crates,build,number) + end + end + end + else + self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end -- number > 0 return self end @@ -1653,7 +1820,8 @@ end -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Group#UNIT Unit -- @param #CTLD.Buildable Build -function CTLD:_BuildObjectFromCrates(Group,Unit,Build) +-- @param #boolean Repair If true this is a repair and not a new build +function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair) self:T(self.lid .. " _BuildObjectFromCrates") -- Spawn-a-crate-content local position = Unit:GetCoordinate() or Group:GetCoordinate() @@ -1667,15 +1835,26 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build) local randomcoord = zone:GetRandomCoordinate(35):GetVec2() for _,_template in pairs(temptable) do self.TroopCounter = self.TroopCounter + 1 + if canmove then local alias = string.format("%s-%d", _template, math.random(1,100000)) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,20,2) :InitDelayOff() :SpawnFromVec2(randomcoord) + else -- don't random position of e.g. SAM units build as FOB + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + --:InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end if self.movetroopstowpzone and canmove then self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) end - self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + if Repair then + self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + else + self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + end end -- template loop return self end @@ -1687,17 +1866,14 @@ function CTLD:_MoveGroupToZone(Group) self:T(self.lid .. " _MoveGroupToZone") local groupname = Group:GetName() or "none" local groupcoord = Group:GetCoordinate() - --self:Tself.lid .. " _MoveGroupToZone for " .. groupname) -- Get closest zone of type local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE) --self:Tstring.format("Closest WP zone %s is %d meters",name,distance)) if (distance <= self.movetroopsdistance) and zone then -- yes, we can ;) local groupname = Group:GetName() - --self:Tstring.format("Moving troops %s to zone %s, distance %d!",groupname,name,distance)) local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE local coordinate = zonecoord:GetVec2() - --self:T{coordinate=coordinate}) Group:SetAIOn() Group:OptionAlarmStateAuto() Group:OptionDisperseOnAttack(30) @@ -1716,7 +1892,6 @@ function CTLD:_CleanUpCrates(Crates,Build,Number) self:T(self.lid .. " _CleanUpCrates") -- clean up real world crates local build = Build -- #CTLD.Buildable - --self:T{Build = Build}) local existingcrates = self.Spawned_Cargo -- #table of exising crates local newexcrates = {} -- get right number of crates to destroy @@ -1730,18 +1905,15 @@ function CTLD:_CleanUpCrates(Crates,Build,Number) for _,_crate in pairs(Crates) do local nowcrate = _crate -- #CTLD_CARGO local name = nowcrate:GetName() - --self:Tstring.format("Looking for Crate for %s", name)) local thisID = nowcrate:GetID() if name == nametype then -- matching crate type table.insert(destIDs,thisID) found = found + 1 nowcrate:GetPositionable():Destroy() nowcrate.Positionable = nil - --self:Tstring.format("%s Found %d Need %d", name, found, numberdest)) end if found == numberdest then break end -- got enough end - --self:T{destIDs}) -- loop and remove from real world representation for _,_crate in pairs(existingcrates) do local excrate = _crate -- #CTLD_CARGO @@ -1766,7 +1938,6 @@ function CTLD:_RefreshF10Menus() self:T(self.lid .. " _RefreshF10Menus") local PlayerSet = self.PilotGroups -- Core.Set#SET_GROUP local PlayerTable = PlayerSet:GetSetObjects() -- #table of #GROUP objects - --self:T({PlayerTable=PlayerTable}) -- rebuild units table local _UnitList = {} for _key, _group in pairs (PlayerTable) do @@ -1783,7 +1954,6 @@ function CTLD:_RefreshF10Menus() self.CtldUnits = _UnitList -- build unit menus - local menucount = 0 local menus = {} for _, _unitName in pairs(self.CtldUnits) do @@ -1795,7 +1965,6 @@ function CTLD:_RefreshF10Menus() -- get chopper capabilities local unittype = _unit:GetTypeName() local capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities - --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities local cantroops = capabilities.troops local cancrates = capabilities.crates -- top menu @@ -1818,7 +1987,8 @@ function CTLD:_RefreshF10Menus() end listmenu = MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates, self._ListCratesNearby, self, _group, _unit) local unloadmenu = MENU_GROUP_COMMAND:New(_group,"Drop crates",topcrates, self._UnloadCrates, self, _group, _unit) - local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit):Refresh() + local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit) + local repairmenu = MENU_GROUP_COMMAND:New(_group,"Repair",topcrates, self._RepairCrates, self, _group, _unit):Refresh() end -- sub menu troops management if cantroops then @@ -1829,6 +1999,7 @@ function CTLD:_RefreshF10Menus() menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) end local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() + local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() end local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) if unittype == "Hercules" then @@ -1876,6 +2047,21 @@ function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates) return self end +--- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. +-- @param #CTLD self +-- @param #string Name Unique name of this type of cargo. E.g. "Humvee". +-- @param #string Template Template of VEHICLE or FOB cargo that this can repair. +-- @param #CTLD_CARGO.Enum Type Type of cargo, here REPAIR. +-- @param #number NoCrates Number of crates needed to build this cargo. +function CTLD:AddCratesRepair(Name,Template,Type,NoCrates) + self:T(self.lid .. " AddCratesRepair") + self.CargoCounter = self.CargoCounter + 1 + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates) + table.insert(self.Cargo_Crates,cargo) + return self +end + --- User function - Add a #CTLD.CargoZoneType zone for this CTLD instance. -- @param #CTLD self -- @param #CTLD.CargoZone Zone Zone #CTLD.CargoZone describing the zone. @@ -1884,13 +2070,10 @@ function CTLD:AddZone(Zone) local zone = Zone -- #CTLD.CargoZone if zone.type == CTLD.CargoZoneType.LOAD then table.insert(self.pickupZones,zone) - --self:T"Registered LOAD zone " .. zone.name) elseif zone.type == CTLD.CargoZoneType.DROP then table.insert(self.dropOffZones,zone) - --self:T"Registered DROP zone " .. zone.name) else table.insert(self.wpZones,zone) - --self:T"Registered MOVE zone " .. zone.name) end return self end @@ -1904,15 +2087,15 @@ function CTLD:ActivateZone(Name,ZoneType,NewState) self:T(self.lid .. " AddZone") local newstate = true -- set optional in case we\'re deactivating - if not NewState or NewState == false then - newstate = false - end + if NewState ~= nil then + newstate = NewState + end + -- get correct table - local zone = ZoneType -- #CTLD.CargoZone local table = {} - if zone.type == CTLD.CargoZoneType.LOAD then + if ZoneType == CTLD.CargoZoneType.LOAD then table = self.pickupZones - elseif zone.type == CTLD.CargoZoneType.DROP then + elseif ZoneType == CTLD.CargoZoneType.DROP then table = self.dropOffZones else table = self.wpZones @@ -1928,6 +2111,7 @@ function CTLD:ActivateZone(Name,ZoneType,NewState) return self end + --- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance. -- @param #CTLD self -- @param #string Name Name of the zone to change in the ME. @@ -1955,7 +2139,6 @@ function CTLD:_GetFMBeacon(Name) beacon.name = Name beacon.frequency = FM / 1000000 beacon.modulation = radio.modulation.FM - return beacon end @@ -2062,11 +2245,10 @@ function CTLD:_ListRadioBeacons(Group, Unit) end end if report:GetCount() == 1 then - report:Add("--------- N O N E ------------") + report:Add(" N O N E") end report:Add("------------------------------------------------------------") self:_SendMessage(report:Text(), 30, true, Group) - --local m = MESSAGE:New(report:Text(),30,"CTLD",true):ToGroup(Group) return self end @@ -2129,7 +2311,6 @@ end function CTLD:IsUnitInZone(Unit,Zonetype) self:T(self.lid .. " IsUnitInZone") local unitname = Unit:GetName() - --self:Tstring.format("%s | Zone search for %s | Type %s",self.lid,unitname,Zonetype)) local zonetable = {} local outcome = false if Zonetype == CTLD.CargoZoneType.LOAD then @@ -2155,7 +2336,6 @@ function CTLD:IsUnitInZone(Unit,Zonetype) local color = czone.color local zoneradius = zone:GetRadius() local distance = self:_GetDistance(zonecoord,unitcoord) - --self:Tstring.format("Check distance: %d",distance)) if distance <= zoneradius and active then outcome = true end @@ -2166,7 +2346,6 @@ function CTLD:IsUnitInZone(Unit,Zonetype) colorret = color end end - --self:T{outcome, zonenameret, zoneret, maxdist}) return outcome, zonenameret, zoneret, maxdist end @@ -2202,7 +2381,6 @@ function CTLD:SmokeZoneNearBy(Unit, Flare) local txt = "smoking" if Flare then txt = "flaring" end self:_SendMessage(string.format("Roger, %s zone %s!",txt, zonename), 10, false, Group) - --local m = MESSAGE:New(string.format("Roger, %s zone %s!",txt, zonename),10,"CTLD"):ToGroup(Group) smoked = true end end @@ -2210,7 +2388,6 @@ function CTLD:SmokeZoneNearBy(Unit, Flare) if not smoked then local distance = UTILS.MetersToNM(self.smokedistance) self:_SendMessage(string.format("Negative, need to be closer than %dnm to a zone!",distance), 10, false, Group) - --local m = MESSAGE:New(string.format("Negative, need to be closer than %dnm to a zone!",distance),10,"CTLD"):ToGroup(Group) end return self end @@ -2263,7 +2440,6 @@ end local maxh = self.maximumHoverHeight -- 15 local minh = self.minimumHoverHeight -- 5 local mspeed = 2 -- 2 m/s - --self:Tstring.format("%s Unit parameters: at %dm AGL with %dmps",self.lid,aheight,uspeed)) if (uspeed <= mspeed) and (aheight <= maxh) and (aheight >= minh) then -- yep within parameters outcome = true @@ -2289,10 +2465,12 @@ end local aheight = uheight - gheight -- height above ground local maxh = self.HercMinAngels-- 1500m local minh = self.HercMaxAngels -- 5000m - local mspeed = 2 -- 2 m/s - -- TODO:Add speed test for Herc, should not be above 280kph/150kn - --self:Tstring.format("%s Unit parameters: at %dm AGL with %dmps",self.lid,aheight,uspeed)) - if (aheight <= maxh) and (aheight >= minh) then + local maxspeed = self.HercMaxSpeed -- 77 mps + -- DONE: TEST - Speed test for Herc, should not be above 280kph/150kn + local kmspeed = uspeed * 3.6 + local knspeed = kmspeed / 1.86 + self:T(string.format("%s Unit parameters: at %dm AGL with %dmps | %dkph | %dkn",self.lid,aheight,uspeed,kmspeed,knspeed)) + if (aheight <= maxh) and (aheight >= minh) and (uspeed <= maxspeed) then -- yep within parameters outcome = true end @@ -2308,9 +2486,15 @@ end local inhover = self:IsCorrectHover(Unit) local htxt = "true" if not inhover then htxt = "false" end - local text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", self.minimumHoverHeight, self.maximumHoverHeight, htxt) + local text = "" + if _SETTINGS:IsMetric() then + text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", self.minimumHoverHeight, self.maximumHoverHeight, htxt) + else + local minheight = UTILS.MetersToFeet(self.minimumHoverHeight) + local maxheight = UTILS.MetersToFeet(self.maximumHoverHeight) + text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 6fts \n - In parameter: %s", minheight, maxheight, htxt) + end self:_SendMessage(text, 10, false, Group) - --local m = MESSAGE:New(text,10,"CTLD",false):ToGroup(Group) return self end @@ -2322,11 +2506,17 @@ end local inhover = self:IsCorrectFlightParameters(Unit) local htxt = "true" if not inhover then htxt = "false" end - local minheight = UTILS.MetersToFeet(self.HercMinAngels) - local maxheight = UTILS.MetersToFeet(self.HercMaxAngels) - local text = string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", minheight, maxheight, htxt) + local text = "" + if _SETTINGS:IsImperial() then + local minheight = UTILS.MetersToFeet(self.HercMinAngels) + local maxheight = UTILS.MetersToFeet(self.HercMaxAngels) + text = string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", minheight, maxheight, htxt) + else + local minheight = self.HercMinAngels + local maxheight = self.HercMaxAngels + text = string.format("Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", minheight, maxheight, htxt) + end self:_SendMessage(text, 10, false, Group) - --local m = MESSAGE:New(text,15,"CTLD",false):ToGroup(Group) return self end @@ -2374,7 +2564,6 @@ end local unitname = Unit:GetName() local Group = Unit:GetGroup() local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities - --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number if cancrates then @@ -2407,6 +2596,20 @@ end return self end + --- (Internal) Run through DroppedTroops and capture alive units + -- @param #CTLD self + -- @return #CTLD self + function CTLD:CleanDroppedTroops() + local troops = self.DroppedTroops + local newtable = {} + for _index, _group in pairs (troops) do + if _group and _group:IsAlive() then + newtable[_index] = _group + end + end + self.DroppedTroops = newtable + return self + end ------------------------------------------------------------------- -- FSM functions ------------------------------------------------------------------- @@ -2419,18 +2622,15 @@ end -- @return #CTLD self function CTLD:onafterStart(From, Event, To) self:T({From, Event, To}) + self:I(self.lid .. "Started.") if self.useprefix or self.enableHercules then local prefix = self.prefixes - --self:T{prefix=prefix}) if self.enableHercules then - --self:T("CTLD with prefixes and Hercules") self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterStart() else - --self:T("CTLD with prefixes NO Hercules") self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterCategories("helicopter"):FilterStart() end else - --self:T("CTLD NO prefixes NO Hercules") self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategories("helicopter"):FilterStart() end -- Events @@ -2449,6 +2649,7 @@ end -- @return #CTLD self function CTLD:onbeforeStatus(From, Event, To) self:T({From, Event, To}) + self:CleanDroppedTroops() self:_RefreshF10Menus() self:_RefreshRadioBeacons() self:CheckAutoHoverload() @@ -2466,8 +2667,7 @@ end -- gather some stats -- pilots local pilots = 0 - for _,_pilot in pairs (self.CtldUnits) do - + for _,_pilot in pairs (self.CtldUnits) do pilots = pilots + 1 end @@ -2530,6 +2730,21 @@ end return self end + --- (Internal) FSM Function onbeforeTroopsExtracted. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + function CTLD:onbeforeTroopsExtracted(From, Event, To, Group, Unit, Troops) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeTroopsDeployed. -- @param #CTLD self -- @param #string From State. diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 7ee0a79fd..aee683f96 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -56,30 +56,30 @@ -- -- ## set up a detection SET_GROUP -- --- `Red_DetectionSetGroup = SET_GROUP:New()` --- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` --- `Red_DetectionSetGroup:FilterOnce()` +-- `Red_DetectionSetGroup = SET_GROUP:New()` +-- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` +-- `Red_DetectionSetGroup:FilterOnce()` -- -- ## New Intel type detection for the red side, logname "KGB" -- --- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` --- `RedIntel:SetClusterAnalysis(true,true)` --- `RedIntel:SetVerbosity(2)` --- `RedIntel:Start()` +-- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` +-- `RedIntel:SetClusterAnalysis(true,true)` +-- `RedIntel:SetVerbosity(2)` +-- `RedIntel:__Start(2)` -- -- ## Hook into new contacts found -- --- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` --- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` +-- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` +-- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` +-- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` +-- `end` -- -- ## And/or new clusters found -- --- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` --- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` +-- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` +-- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` +-- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` +-- `end` -- -- -- @field #INTEL @@ -116,7 +116,7 @@ INTEL = { -- @field #number speed Last known speed in m/s. -- @field #boolean isship -- @field #boolean ishelo --- @field #boolean isgrund +-- @field #boolean isground -- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact -- @field #string recce The name of the recce unit that detected this contact @@ -135,13 +135,13 @@ INTEL = { --- INTEL class version. -- @field #string version -INTEL.version="0.2.2" +INTEL.version="0.2.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Filter detection methods. +-- DONE: Filter detection methods. -- TODO: process detected set asynchroniously for better performance. -- DONE: Accept zones. -- DONE: Reject zones. @@ -203,7 +203,14 @@ function INTEL:New(DetectionSet, Coalition, Alias) self.alias="CIA" end end - end + end + + self.DetectVisual = true + self.DetectOptical = true + self.DetectRadar = true + self.DetectIRST = true + self.DetectRWR = true + self.DetectDLINK = true -- Set some string id for output to DCS.log file. self.lid=string.format("INTEL %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") @@ -222,7 +229,8 @@ function INTEL:New(DetectionSet, Coalition, Alias) self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more. self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected. - self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. + self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. + self:AddTransition("*", "Stop", "Stopped") -- Defaults self:SetForgetTime() @@ -475,6 +483,47 @@ function INTEL:SetClusterRadius(radius) return self end +--- Set detection types for this #INTEL - all default to true. +-- @param #INTEL self +-- @param #boolean DetectVisual Visual detection +-- @param #boolean DetectOptical Optical detection +-- @param #boolean DetectRadar Radar detection +-- @param #boolean DetectIRST IRST detection +-- @param #boolean DetectRWR RWR detection +-- @param #boolean DetectDLINK Data link detection +-- @return self +function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + self.DetectVisual = DetectVisual and true + self.DetectOptical = DetectOptical and true + self.DetectRadar = DetectRadar and true + self.DetectIRST = DetectIRST and true + self.DetectRWR = DetectRWR and true + self.DetectDLINK = DetectDLINK and true + return self +end + +--- Get table of #INTEL.Contact objects +-- @param #INTEL self +-- @return #table Contacts or nil if not running +function INTEL:GetContactTable() + if self:Is("Running") then + return self.Contacts + else + return nil + end +end + +--- Get table of #INTEL.Cluster objects +-- @param #INTEL self +-- @return #table Clusters or nil if not running +function INTEL:GetClusterTable() + if self:Is("Running") and self.clusteranalysis then + return self.Clusters + else + return nil + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -530,7 +579,7 @@ function INTEL:onafterStatus(From, Event, To) text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.group:CountAliveUnits(), dT) if contact.mission then local mission=contact.mission --Ops.Auftrag#AUFTRAG - text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unkown") + text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end end self:I(self.lid..text) @@ -558,14 +607,13 @@ function INTEL:UpdateIntel() local recce=_recce --Wrapper.Unit#UNIT -- Get detected units. - self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting) + self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK) end end end - -- TODO: Filter detection methods? local remove={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT @@ -700,7 +748,7 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() item.recce=RecceDetecting[groupname] - self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknonw", item.recce or "unknown")) + self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) -- Add contact to table. self:AddContact(item) @@ -728,7 +776,7 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) end ---- Return the detected target groups of the controllable as a @{SET_GROUP}. +--- (Internal) Return the detected target groups of the controllable as a @{SET_GROUP}. -- The optional parametes specify the detection methods that can be applied. -- If no detection method is given, the detection will use all the available methods by default. -- @param #INTEL self @@ -815,7 +863,7 @@ function INTEL:onafterLostCluster(From, Event, To, Cluster, Mission) local text = self.lid..string.format("LOST cluster %d", Cluster.index) if Mission then local mission=Mission --Ops.Auftrag#AUFTRAG - text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unkown") + text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end self:T(text) end @@ -1391,3 +1439,306 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------- +-- Start INTEL_DLINK +---------------------------------------------------------------------------------------------- + +--- **Ops_DLink** - Support for Office of Military Intelligence. +-- +-- **Main Features:** +-- +-- * Overcome limitations of (non-available) datalinks between ground radars +-- * Detect and track contacts consistently across INTEL instances +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +--- === +-- +-- ### Author: **applevangelist** + +--- INTEL_DLINK class. +-- @type INTEL_DLINK +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number verbose Make the logging verbose. +-- @field #string alias Alias name for logging. +-- @field #number cachetime Number of seconds to keep an object. +-- @field #number interval Number of seconds between collection runs. +-- @field #table contacts Table of Ops.Intelligence#INTEL.Contact contacts. +-- @field #table clusters Table of Ops.Intelligence#INTEL.Cluster clusters. +-- @field #table contactcoords Table of contacts' Core.Point#COORDINATE objects. +-- @extends Core.Fsm#FSM + +--- INTEL_DLINK data aggregator +-- @field #INTEL_DLINK +INTEL_DLINK = { + ClassName = "INTEL_DLINK", + verbose = 0, + lid = nil, + alias = nil, + cachetime = 300, + interval = 20, + contacts = {}, + clusters = {}, + contactcoords = {}, +} + +--- Version string +-- @field #string version +INTEL_DLINK.version = "0.0.1" + +--- Function to instantiate a new object +-- @param #INTEL_DLINK self +-- @param #table Intels Table of Ops.Intelligence#INTEL objects. +-- @param #string Alias (optional) Name of this instance. Default "SPECTRE" +-- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds). +-- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds). +-- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a +-- Data Link, e.g. for Russian-tech based EWR, realising a Star Topology @{https://en.wikipedia.org/wiki/Network_topology#Star} +-- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. +-- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP). +-- +-- Basic setup: +-- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) +-- datalink:__Start(2) +-- +-- Add an Intel while running: +-- datalink:AddIntel(myintel3) +-- +-- Gather the data: +-- datalink:GetContactTable() -- #table of #INTEL.Contact contacts. +-- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. +-- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. +-- +-- Gather data with the event function: +-- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) +-- ... ... +-- end +-- +function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #INTEL + + self.intels = Intels or {} + self.contacts = {} + self.clusters = {} + self.contactcoords = {} + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="SPECTRE" + end + + -- Cache time + self.cachetime = Cachetime or 300 + + -- Interval + self.interval = Interval or 20 + + -- Set some string id for output to DCS.log file. + self.lid=string.format("INTEL_DLINK %s | ", self.alias) + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Collect", "*") -- Collect data. + self:AddTransition("*", "Collected", "*") -- Collection of data done. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + ---------------------------------------------------------------------------------------------- + -- Pseudo Functions + ---------------------------------------------------------------------------------------------- + --- Triggers the FSM event "Start". Starts the INTEL_DLINK. + -- @function [parent=#INTEL_DLINK] Start + -- @param #INTEL_DLINK self + + --- Triggers the FSM event "Start" after a delay. Starts the INTEL_DLINK. + -- @function [parent=#INTEL_DLINK] __Start + -- @param #INTEL_DLINK self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the INTEL_DLINK. + -- @param #INTEL_DLINK self + + --- Triggers the FSM event "Stop" after a delay. Stops the INTEL_DLINK. + -- @function [parent=#INTEL_DLINK] __Stop + -- @param #INTEL_DLINK self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Collect". Used internally to collect all data. + -- @function [parent=#INTEL_DLINK] Collect + -- @param #INTEL_DLINK self + + --- Triggers the FSM event "Collect" after a delay. + -- @function [parent=#INTEL_DLINK] __Status + -- @param #INTEL_DLINK self + -- @param #number delay Delay in seconds. + + --- On After "Collected" event. Data tables have been refreshed. + -- @function [parent=#INTEL_DLINK] OnAfterCollected + -- @param #INTEL_DLINK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table Contacts Table of #INTEL.Contact Contacts. + -- @param #table Clusters Table of #INTEL.Cluster Clusters. + + return self +end +---------------------------------------------------------------------------------------------- +-- Helper & User Functions +---------------------------------------------------------------------------------------------- + +--- Function to add an #INTEL object to the aggregator +-- @param #INTEL_DLINK self +-- @param Ops.Intelligence#INTEL Intel the #INTEL object to add +-- @return #INTEL_DLINK self +function INTEL_DLINK:AddIntel(Intel) + self:T(self.lid .. "AddIntel") + if Intel then + table.insert(self.intels,Intel) + end + return self +end + +---------------------------------------------------------------------------------------------- +-- FSM Functions +---------------------------------------------------------------------------------------------- + +--- Function to start the work. +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onafterStart(From, Event, To) + self:T({From, Event, To}) + local text = string.format("Version %s started.", self.version) + self:I(self.lid .. text) + self:__Collect(-math.random(1,10)) + return self +end + +--- Function to collect data from the various #INTEL +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollect(From, Event, To) + self:T({From, Event, To}) + -- run through our #INTEL objects and gather the contacts tables + self:T("Contacts Data Gathering") + local newcontacts = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetContactTable() or {} -- #INTEL.Contact + for _,_contact in pairs (ctable) do + local _ID = string.format("%s-%d",_contact.groupname, _contact.Tdetected) + self:T(string.format("Adding %s",_ID)) + newcontacts[_ID] = _contact + end + end + end + -- clean up for stale contacts and dupes + self:T("Cleanup") + local contacttable = {} + local coordtable = {} + local TNow = timer.getAbsTime() + local Tcache = self.cachetime + for _ind, _contact in pairs(newcontacts) do -- #string, #INTEL.Contact + if TNow - _contact.Tdetected < Tcache then + if (not contacttable[_contact.groupname]) or (contacttable[_contact.groupname] and contacttable[_contact.groupname].Tdetected < _contact.Tdetected) then + self:T(string.format("Adding %s",_contact.groupname)) + contacttable[_contact.groupname] = _contact + table.insert(coordtable,_contact.position) + end + end + end + -- run through our #INTEL objects and gather the clusters tables + self:T("Clusters Data Gathering") + local newclusters = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetClusterTable() or {} -- #INTEL.Cluster + for _,_cluster in pairs (ctable) do + local _ID = string.format("%s-%d", _intel.alias, _cluster.index) + self:T(string.format("Adding %s",_ID)) + table.insert(newclusters,_cluster) + end + end + end + -- update self tables + self.contacts = contacttable + self.contactcoords = coordtable + self.clusters = newclusters + self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event + -- schedule next round + local interv = self.interval * -1 + self:__Collect(interv) + return self +end + +--- Function called after collection is done +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @param #table Contacts The table of collected #INTEL.Contact contacts +-- @param #table Clusters The table of collected #INTEL.Cluster clusters +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollected(From, Event, To, Contacts, Clusters) + self:T({From, Event, To}) + return self +end + +--- Function to stop +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onafterStop(From, Event, To) + self:T({From, Event, To}) + local text = string.format("Version %s stopped.", self.version) + self:I(self.lid .. text) + return self +end + +--- Function to query the detected contacts +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Contact contacts +function INTEL_DLINK:GetContactTable() + self:T(self.lid .. "GetContactTable") + return self.contacts +end + +--- Function to query the detected clusters +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Cluster clusters +function INTEL_DLINK:GetClusterTable() + self:T(self.lid .. "GetClusterTable") + return self.clusters +end + +--- Function to query the detected contact coordinates +-- @param #INTEL_DLINK self +-- @return #table Table of the contacts' Core.Point#COORDINATE objects. +function INTEL_DLINK:GetDetectedItemCoordinates() + self:T(self.lid .. "GetDetectedItemCoordinates") + return self.contactcoords +end + +---------------------------------------------------------------------------------------------- +-- End INTEL_DLINK +---------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index d53f1456a..9829b9ac1 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1668,4 +1668,149 @@ function UTILS.IsLoadingDoorOpen( unit_name ) end -- nil return nil +end + +--- Function to generate valid FM frequencies in mHz for radio beacons (FM). +-- @return #table Table of frequencies. +function UTILS.GenerateFMFrequencies() + local FreeFMFrequencies = {} + for _first = 3, 7 do + for _second = 0, 5 do + for _third = 0, 9 do + local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit + table.insert(FreeFMFrequencies, _frequency) + end + end + end + return FreeFMFrequencies +end + +--- Function to generate valid VHF frequencies in kHz for radio beacons (FM). +-- @return #table VHFrequencies +function UTILS.GenerateVHFrequencies() + + -- known and sorted map-wise NDBs in kHz + local _skipFrequencies = { + 214,274,291.5,295,297.5, + 300.5,304,307,309.5,311,312,312.5,316, + 320,324,328,329,330,332,336,337, + 342,343,348,351,352,353,358, + 363,365,368,372.5,374, + 380,381,384,385,389,395,396, + 414,420,430,432,435,440,450,455,462,470,485, + 507,515,520,525,528,540,550,560,570,577,580, + 602,625,641,662,670,680,682,690, + 705,720,722,730,735,740,745,750,770,795, + 822,830,862,866, + 905,907,920,935,942,950,995, + 1000,1025,1030,1050,1065,1116,1175,1182,1210 + } + + local FreeVHFFrequencies = {} + + -- first range + local _start = 200000 + while _start < 400000 do + + -- skip existing NDB frequencies# + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 10000 + end + + -- second range + _start = 400000 + while _start < 850000 do + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 10000 + end + + -- third range + _start = 850000 + while _start <= 999000 do -- adjusted for Gazelle + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 50000 + end + + return FreeVHFFrequencies +end + +--- Function to generate valid UHF Frequencies in mHz (AM). +-- @return #table UHF Frequencies +function UTILS.GenerateUHFrequencies() + + local FreeUHFFrequencies = {} + local _start = 220000000 + + while _start < 399000000 do + table.insert(FreeUHFFrequencies, _start) + _start = _start + 500000 + end + + return FreeUHFFrequencies +end + +--- Function to generate valid laser codes for JTAC. +-- @return #table Laser Codes. +function UTILS.GenerateLaserCodes() + local jtacGeneratedLaserCodes = {} + + -- helper function + local function ContainsDigit(_number, _numberToFind) + local _thisNumber = _number + local _thisDigit = 0 + while _thisNumber ~= 0 do + _thisDigit = _thisNumber % 10 + _thisNumber = math.floor(_thisNumber / 10) + if _thisDigit == _numberToFind then + return true + end + end + return false + end + + -- generate list of laser codes + local _code = 1111 + local _count = 1 + while _code < 1777 and _count < 30 do + while true do + _code = _code + 1 + if not self:_ContainsDigit(_code, 8) + and not ContainsDigit(_code, 9) + and not ContainsDigit(_code, 0) then + table.insert(jtacGeneratedLaserCodes, _code) + break + end + end + _count = _count + 1 + end + return jtacGeneratedLaserCodes end \ No newline at end of file